From 16ee77b06983fa5eddc2993d61b016dc7d0444f3 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Sat, 18 Jan 2025 21:54:18 -0500 Subject: [PATCH 1/3] Refactor: settings.gradle includes on separate lines --- settings.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 2f60ba18..0fd8d717 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,11 @@ rootProject.name = 'bosk' -include 'bosk-annotations', 'bosk-core', 'bosk-jackson', 'bosk-logback', 'bosk-mongo', 'bosk-spring-boot-3', 'bosk-testing', 'lib-testing', 'example-hello' +include 'bosk-annotations' +include 'bosk-core' +include 'bosk-jackson' +include 'bosk-logback' +include 'bosk-mongo' +include 'bosk-spring-boot-3' +include 'bosk-testing' +include 'example-hello' +include 'lib-testing' From 7d800029ebcfde1f123e3e6fa4ad11ccf8d0c1e5 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Sun, 19 Jan 2025 10:06:00 -0500 Subject: [PATCH 2/3] Refactor: rename assertRoundTripWorks --- .../bosk/drivers/mongo/BsonSurgeonTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java index 31edc2a8..41a44e71 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java @@ -78,34 +78,34 @@ void setup() throws InvalidTypeException, IOException, InterruptedException { @Test void root_roundTripWorks() { - doTest(bosk.rootReference()); + assertRoundTripWorks(bosk.rootReference()); } @Test void catalog_roundTripWorks() { - doTest(refs.catalog()); + assertRoundTripWorks(refs.catalog()); } @Test void catalogEntry_roundTripWorks() { - doTest(refs.entity(Identifier.from("entity1"))); + assertRoundTripWorks(refs.entity(Identifier.from("entity1"))); } @Test void nestedCatalog_roundTripWorks() { - doTest(refs.nestedCatalog(Identifier.from("entity1"))); + assertRoundTripWorks(refs.nestedCatalog(Identifier.from("entity1"))); } @Test void childEntry_roundTripWorks() { - doTest(refs.child( + assertRoundTripWorks(refs.child( Identifier.from("entity1"), Identifier.from("child1"))); } @Test void grandchildEntry_roundTripWorks() { - doTest(refs.grandchild( + assertRoundTripWorks(refs.grandchild( Identifier.from("entity1"), Identifier.from("child1"), Identifier.from("child1"))); @@ -113,7 +113,7 @@ void grandchildEntry_roundTripWorks() { @Test void sideTable_roundTripWorks() { - doTest(refs.sideTable()); + assertRoundTripWorks(refs.sideTable()); } @Test @@ -188,7 +188,7 @@ void duplicatePaths_throws() { }); } - private void doTest(Reference mainRef) { + private void assertRoundTripWorks(Reference mainRef) { BsonDocument entireDoc; try (var _ = bosk.readContext()) { entireDoc = (BsonDocument) formatter.object2bsonValue(mainRef.value(), mainRef.targetType()); From 45684ff118796304d042181e6f7453ed7f6365b5 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Sat, 18 Jan 2025 21:56:14 -0500 Subject: [PATCH 3/3] Split BSON functionality into its own library. BSON turns out to be a convenient mutable representation of state trees. It can be used outside the context of MongoDB. --- bosk-bson/README.md | 7 + bosk-bson/build.gradle | 47 +++ .../works/bosk/bson}/BsonFormatException.java | 2 +- .../java/works/bosk/bson/BsonFormatter.java | 332 ++++++++++++++++++ .../java/works/bosk/bson}/BsonPlugin.java | 7 +- .../java/works/bosk/bson}/BsonSurgeon.java | 60 ++-- .../works/bosk/bson/BsonFormatterTest.java | 51 +-- .../java/works/bosk/bson}/BsonPluginTest.java | 2 +- .../works/bosk/bson}/BsonSurgeonTest.java | 19 +- .../works/bosk/bson/DottedFieldNameTest.java | 10 +- .../works/bosk/bson/DottedFieldNameTest2.java | 44 +++ bosk-core/src/main/java/works/bosk/Path.java | 3 + .../src/main/java/works/bosk/Reference.java | 1 + bosk-mongo/build.gradle | 1 + .../works/bosk/drivers/mongo/Formatter.java | 255 +------------- .../works/bosk/drivers/mongo/MainDriver.java | 3 +- .../works/bosk/drivers/mongo/MongoDriver.java | 1 + .../bosk/drivers/mongo/PandoFormatDriver.java | 46 +-- .../drivers/mongo/SequoiaFormatDriver.java | 5 +- .../mongo/AbstractMongoDriverTest.java | 1 + .../drivers/mongo/MiscMongoDriverTest.java | 14 + .../mongo/MongoDriverConformanceTest.java | 1 + .../drivers/mongo/MongoDriverHanoiTest.java | 1 + .../drivers/mongo/MongoDriverSpecialTest.java | 1 + .../drivers/mongo/SchemaEvolutionTest.java | 3 + .../drivers/mongo/example/ExampleBosk.java | 2 +- lib-testing/build.gradle | 2 +- .../works/bosk/AbstractRoundTripTest.java | 3 +- settings.gradle | 1 + 29 files changed, 549 insertions(+), 376 deletions(-) create mode 100644 bosk-bson/README.md create mode 100644 bosk-bson/build.gradle rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonFormatException.java (88%) create mode 100644 bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonPlugin.java (99%) rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonSurgeon.java (83%) rename bosk-mongo/src/test/java/works/bosk/drivers/mongo/FormatterTest.java => bosk-bson/src/test/java/works/bosk/bson/BsonFormatterTest.java (64%) rename {bosk-mongo/src/test/java/works/bosk/drivers/mongo => bosk-bson/src/test/java/works/bosk/bson}/BsonPluginTest.java (98%) rename {bosk-mongo/src/test/java/works/bosk/drivers/mongo => bosk-bson/src/test/java/works/bosk/bson}/BsonSurgeonTest.java (94%) rename bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverDottedFieldNameTest.java => bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest.java (92%) create mode 100644 bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest2.java create mode 100644 bosk-mongo/src/test/java/works/bosk/drivers/mongo/MiscMongoDriverTest.java diff --git a/bosk-bson/README.md b/bosk-bson/README.md new file mode 100644 index 00000000..5e4e4529 --- /dev/null +++ b/bosk-bson/README.md @@ -0,0 +1,7 @@ +## bosk-bson + +This is the subproject for the published `bosk-bson` library, +containing facilities for manipulating MongoDB's binary JSON data structures. +Outside of MongoDB itself, we also use BSON as a handy utility +for divide up and re-combine large JSON documents, +which is useful even outside of MongoDB. diff --git a/bosk-bson/build.gradle b/bosk-bson/build.gradle new file mode 100644 index 00000000..648ba9df --- /dev/null +++ b/bosk-bson/build.gradle @@ -0,0 +1,47 @@ + +plugins { + id 'bosk.development' + id 'bosk.maven-publish' + id 'info.solidsoft.pitest' version '1.15.0' + id 'com.github.spotbugs' version '5.1.5' +} + +base { + archivesName = 'bosk-bson' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(jdkVersion) + } +} + +compileJava { + options.release = prodJavaVersion +} + +compileTestJava { + options.release = null +} + +dependencies { + api project(":bosk-core") + api 'org.mongodb:bson:5.1.2' + implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' // To stop warnings about When from MongoDB driver + + testImplementation project(":bosk-logback") + testImplementation project(":bosk-testing") + testImplementation project(":lib-testing") +} + +pitest { + pitestVersion = '1.15.0' + junit5PluginVersion = '1.2.0' + jvmArgs = ['-ea'] // Our unit tests check for assert statements + targetClasses = ['works.bosk.drivers.mongo.BsonSurgeon'] + targetTests = ['works.bosk.drivers.mongo.BsonSurgeonTest'] + threads = 4 + outputFormats = ['XML', 'HTML'] + timestampedReports = false + //verbose = true +} diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java similarity index 88% rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java rename to bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java index 6b3ecde4..7de567ef 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java +++ b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java @@ -1,4 +1,4 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; class BsonFormatException extends IllegalStateException { public BsonFormatException(String s) { super(s); } diff --git a/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java new file mode 100644 index 00000000..9b36c529 --- /dev/null +++ b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java @@ -0,0 +1,332 @@ +package works.bosk.bson; + +import java.lang.reflect.Type; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import org.bson.BsonDocument; +import org.bson.BsonDocumentWriter; +import org.bson.BsonReader; +import org.bson.BsonValue; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.DocumentCodecProvider; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.ValueCodecProvider; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import works.bosk.BoskDiagnosticContext; +import works.bosk.BoskInfo; +import works.bosk.Listing; +import works.bosk.Path; +import works.bosk.Reference; +import works.bosk.SerializationPlugin; +import works.bosk.SideTable; +import works.bosk.exceptions.InvalidTypeException; + +import static works.bosk.Path.validSegment; +import static works.bosk.ReferenceUtils.rawClass; + +/** + * Utility class for encoding and decoding bosk state trees as BSON. + */ +public class BsonFormatter { + protected static final UnaryOperator DECODER; + protected static final UnaryOperator ENCODER; + protected final CodecRegistry simpleCodecs; + protected final Function> preferredBoskCodecs; + protected final Function, SerializationPlugin.DeserializationScope> deserializationScopeFunction; + + public BsonFormatter(BoskInfo boskInfo, BsonPlugin bsonPlugin) { + this.simpleCodecs = CodecRegistries.fromProviders( + bsonPlugin.codecProviderFor(boskInfo), + new ValueCodecProvider(), + new DocumentCodecProvider()); + this.preferredBoskCodecs = type -> bsonPlugin.getCodec(type, rawClass(type), simpleCodecs, boskInfo); + this.deserializationScopeFunction = bsonPlugin::newDeserializationScope; + } + + public static String dottedFieldNameSegment(String segment) { + return ENCODER.apply(validSegment(segment)); + } + + public static String undottedFieldNameSegment(String dottedSegment) { + return DECODER.apply(dottedSegment); + } + + /** + * @param refLength behave as though ref were truncated to this length without actually having to do it + */ + private static void buildDottedFieldNameOf(Reference ref, int startLength, int refLength, ArrayList segments) { + if (ref.path().length() > startLength) { + Reference enclosingReference = ref.enclosingReference(Object.class); + BsonFormatter.buildDottedFieldNameOf(enclosingReference, startLength, refLength, segments); + if (ref.path().length() <= refLength) { + if (Listing.class.isAssignableFrom(enclosingReference.targetClass())) { + segments.add("ids"); + } else if (SideTable.class.isAssignableFrom(enclosingReference.targetClass())) { + segments.add("valuesById"); + } + segments.add(BsonFormatter.dottedFieldNameSegment(ref.path().lastSegment())); + } + } + } + + static { + DECODER = s->{ + return URLDecoder.decode(s, StandardCharsets.UTF_8); + }; + + ENCODER = s->{ + // Selective percent-encoding of characters MongoDB doesn't like. + // Standard percent-encoding doesn't handle the period character, which + // we want, so if we're already diverging from the standard, we might + // as well do something that suits our needs. + // Good to stay compatible with standard percent-DEcoding, though. + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); ) { + int cp = s.codePointAt(i); + switch (cp) { + case '%': // For percent-encoding + case '+': case ' ': // These two are affected by URLDecoder + case '$': // MongoDB treats these specially + case '.': // MongoDB separator for dotted field names + case 0: // Can MongoDB handle nulls? Probably. Do we want to find out? Not really. + case '|': // (These are reserved for internal use) + case '!': + case '~': + case '[': + case ']': + appendPercentEncoded(sb, cp); + break; + default: + sb.appendCodePoint(cp); + break; + } + i += Character.charCount(cp); + } + return sb.toString(); + }; + } + + private static void appendPercentEncoded(StringBuilder sb, int cp) { + assert 0 <= cp && cp <= 255; + sb + .append('%') + .append(hexCharForDigit(cp / 16)) + .append(hexCharForDigit(cp % 16)); + } + + /** + * An uppercase version of {@link Character#forDigit} with a radix of 16. + */ + private static char hexCharForDigit(int value) { + if (value < 10) { + return (char)('0' + value); + } else { + return (char)('A' + value - 10); + } + } + + /** + * @param ref the bosk node whose field name is required + * @param startingRef the bosk node corresponding to the MongoDB document in which {@code ref} is located + * @return MongoDB field name corresponding to {@code ref} within the document for {@code startingRef}, starting with {@link DocumentFields#state state} + * @see #referenceTo(String, Reference) + */ + public static String dottedFieldNameOf(Reference ref, Reference startingRef) { + assert startingRef.encloses(ref); + return dottedFieldNameOf(ref, ref.path().length(), startingRef); + } + + /** + * @param refLength behave as though ref were truncated to this length, without actually having to do it + * @return MongoDB field name corresponding to the given Reference + * @see #referenceTo(String, Reference) + */ + static String dottedFieldNameOf(Reference ref, int refLength, Reference startingRef) { + // TODO: Is this required? It's currently only really called by tests + ArrayList segments = dottedFieldNameSegments(ref, refLength, startingRef); + return String.join(".", segments.toArray(new String[0])); + } + + /** + * @return Reference corresponding to the given field name within a document representing {@code startingReference} + * @see #dottedFieldNameOf + */ + @SuppressWarnings("unchecked") + public static Reference referenceTo(String dottedName, Reference startingReference) throws InvalidTypeException { + Reference ref = startingReference; + Iterator iter = Arrays.asList(dottedName.split(Pattern.quote("."))).iterator(); + BsonFormatter.skipField(ref, iter, DocumentFields.state.name()); // The entire Bosk state is in this field + while (iter.hasNext()) { + if (Listing.class.isAssignableFrom(ref.targetClass())) { + BsonFormatter.skipField(ref, iter, "ids"); + } else if (SideTable.class.isAssignableFrom(ref.targetClass())) { + BsonFormatter.skipField(ref, iter, "valuesById"); + } + if (iter.hasNext()) { + String segment = undottedFieldNameSegment(iter.next()); + ref = ref.then(Object.class, segment); + } + } + return (Reference) ref; + } + + private static void skipField(Reference ref, Iterator iter, String expectedName) { + String actualName; + try { + actualName = iter.next(); + } catch (NoSuchElementException e) { + throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; encountered end of dotted field name"); + } + if (!expectedName.equals(actualName)) { + throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; was: " + actualName); + } + } + + /** + * A BSON path is a pipe-delimited sequence of BSON fields. + * + *

+ * Note that the BSON structure is such that the BSON path's segments may + * not exactly match those of {@code ref.path()}. + * + *

+ * This computes the BSON path for {@code ref} within a document representing {@code startingRef}. + * + * @param ref reference to the node whose path is desired + * @param startingRef reference to the node corresponding to the document containing the {@code ref} node + * @return the BSON path leading to {@code ref} within a document representing {@code startingRef} + */ + public static String docBsonPath(Reference ref, Reference startingRef) { + return "|" + String.join("|", docSegments(ref, startingRef)); + } + + /** + * @return list of field names suitable for {@link #lookup} to find the document corresponding + * to docRef inside a document corresponding to rootRef + */ + private static List docSegments(Reference docRef, Reference rootRef) { + ArrayList allSegments = dottedFieldNameSegments(docRef, docRef.path().length(), rootRef); + return allSegments + .subList(1, allSegments.size()); // Skip the "state" field + } + + protected Codec codecFor(Type type) { + // BsonPlugin gives better codecs than CodecRegistry, because BsonPlugin is aware of generics, + // so we always try that first. The CodecSupplier protocol uses "null" to indicate that another + // CodecSupplier should be used, so we follow that protocol and fall back on the CodecRegistry. + // TODO: Should this logic be in BsonPlugin? It has nothing to do with MongoDriver really. + Codec result = preferredBoskCodecs.apply(type); + if (result == null) { + return simpleCodecs.get(rawClass(type)); + } else { + return result; + } + } + + /** + * Converts a bosk state node to BSON. + * + *

+ * A common way to call this is, for a given {@link Reference} {@code ref}, is: + *

+	 *     ref.value(), ref.targetType()
+	 * 
+ * @param object the bosk state node to convert + * @param type the type of {@code object} + * @see #bsonValue2object(BsonValue, Reference) + */ + @SuppressWarnings("unchecked") + public BsonValue object2bsonValue(T object, Type type) { + rawClass(type).cast(object); + Codec objectCodec = (Codec) codecFor(type); + BsonDocument document = new BsonDocument(); + try (BsonDocumentWriter writer = new BsonDocumentWriter(document)) { + // To support arbitrary values, not just whole documents, we put the result INSIDE a document. + writer.writeStartDocument(); + writer.writeName("value"); + objectCodec.encode(writer, object, EncoderContext.builder().build()); + writer.writeEndDocument(); + } + return document.get("value"); + } + + /** + * Converts a BSON value to a bosk state node. + * @param bson the value to convert + * @param target the bosk location of the resulting node + * @see #object2bsonValue(Object, Type) + */ + @SuppressWarnings("unchecked") + public T bsonValue2object(BsonValue bson, Reference target) { + Codec objectCodec = (Codec) codecFor(target.targetType()); + BsonDocument document = new BsonDocument(); + document.append("value", bson); + try ( + @SuppressWarnings("unused") SerializationPlugin.DeserializationScope scope = deserializationScopeFunction.apply(target); + BsonReader reader = document.asBsonReader() + ) { + reader.readStartDocument(); + reader.readName("value"); + return objectCodec.decode(reader, DecoderContext.builder().build()); + } + } + + /** + * The fields of the main MongoDB document. Case-sensitive. + * + *

+ * No field name should be a prefix of any other. + */ + public enum DocumentFields { + /** + * The location of this document's state within the conceptual giant document. + * A pipe-separated list of BSON field names. + */ + path, + + /** + * The BSON-encoded portion of the bosk state represented by this document. + */ + state, + + /** + * An ever-increasing 64-bit long that is incremented every time the document changes. + */ + revision, + + /** + * The contents of {@link BoskDiagnosticContext} corresponding to this + * document's last update. + */ + diagnostics, + } + + static ArrayList dottedFieldNameSegments(Reference ref, int refLength, Reference startingRef) { + assert startingRef.path().matchesPrefixOf(ref.path()): "'" + ref + "' must be under '" + startingRef + "'"; + ArrayList segments = new ArrayList<>(); + segments.add(DocumentFields.state.name()); + buildDottedFieldNameOf(ref, startingRef.path().length(), refLength, segments); + return segments; + } + + /** + * @param elementRefLength behave as though elementRef were truncated to this length, without actually having to do it + * @return MongoDB field name corresponding to the object that contains the given element + * @see #referenceTo(String, Reference) + */ + static List containerSegments(Reference elementRef, int elementRefLength, Reference startingRef) { + List elementSegments = dottedFieldNameSegments(elementRef, elementRefLength, startingRef); + return elementSegments.subList(0, elementSegments.size()-1); // Trim off the element itself + } + +} diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java b/bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java similarity index 99% rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java rename to bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java index 17dbed7f..87c9afa8 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java +++ b/bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java @@ -1,11 +1,10 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.RecordComponent; import java.lang.reflect.Type; @@ -66,8 +65,8 @@ import static works.bosk.ReferenceUtils.getterMethod; import static works.bosk.ReferenceUtils.parameterType; import static works.bosk.ReferenceUtils.rawClass; -import static works.bosk.drivers.mongo.Formatter.dottedFieldNameSegment; -import static works.bosk.drivers.mongo.Formatter.undottedFieldNameSegment; +import static works.bosk.bson.BsonFormatter.dottedFieldNameSegment; +import static works.bosk.bson.BsonFormatter.undottedFieldNameSegment; public final class BsonPlugin extends SerializationPlugin { private final ValueCodecProvider valueCodecProvider = new ValueCodecProvider(); diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java b/bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java similarity index 83% rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java rename to bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java index 3fa14d1a..eb9a5b71 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java +++ b/bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java @@ -1,4 +1,4 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import java.util.ArrayList; import java.util.Arrays; @@ -11,27 +11,32 @@ import org.bson.BsonInvalidOperationException; import org.bson.BsonString; import org.bson.BsonValue; -import works.bosk.Bosk; import works.bosk.EnumerableByIdentifier; import works.bosk.Identifier; import works.bosk.Path; import works.bosk.Reference; -import works.bosk.SideTable; +import works.bosk.bson.BsonFormatter.DocumentFields; import works.bosk.exceptions.InvalidTypeException; import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; -import static works.bosk.drivers.mongo.Formatter.containerSegments; -import static works.bosk.drivers.mongo.Formatter.dottedFieldNameSegments; -import static works.bosk.drivers.mongo.Formatter.undottedFieldNameSegment; +import static works.bosk.bson.BsonFormatter.containerSegments; +import static works.bosk.bson.BsonFormatter.undottedFieldNameSegment; /** * Splits up a single large BSON document into multiple self-describing pieces, * and re-assembles them. Provides the core mechanism to carve large BSON structures * into pieces so they can stay under the MongoDB document size limit. + * *

+ * Beware: {@link BsonSurgeon} takes advantage of the mutability of the + * {@link BsonDocument} data structure to perform surgeries efficiently. + * As a consequence, the {@link #scatter} and {@link #gather} methods + * will modify objects passed in as arguments. * + *

* Jargon: *

*
Root document
@@ -40,9 +45,13 @@ *
BSON document corresponding to the part of the state tree being described
*
*/ -class BsonSurgeon { +public class BsonSurgeon { final List graftPoints; + /** + * Internal representation of a graft point, storing a pre-built placeholder + * reference for an entry in the grafted container for efficiency. + */ record GraftPoint ( Reference> containerRef, Reference entryPlaceholderRef @@ -64,9 +73,9 @@ public GraftPoint boundTo(Identifier id) { */ private static final String BSON_PATH_FIELD = "_id"; - private static final String STATE_FIELD = Formatter.DocumentFields.state.name(); + private static final String STATE_FIELD = DocumentFields.state.name(); - BsonSurgeon(List>> graftPoints) { + public BsonSurgeon(List>> graftPoints) { this.graftPoints = new ArrayList<>(graftPoints.size()); graftPoints.stream() // Scatter bottom-up so we don't have to worry about scattering already-scattered documents @@ -75,7 +84,7 @@ public GraftPoint boundTo(Identifier id) { GraftPoint.of(containerRef, entryRef(containerRef)))); } - static Reference entryRef(Reference> containerRef) { + private static Reference entryRef(Reference> containerRef) { // We need a reference pointing all the way to the collection entry, so that if the // collection itself has BSON fields (like SideTable does), those fields will be included // in the dotted name segment list. The actual ID we pick doesn't matter and will be ignored. @@ -94,34 +103,23 @@ static Reference entryRef(Reference> cont * * @param docRef the bosk node corresponding to document * @param document will be modified! - * @param rootRef {@link Bosk#rootReference()} * @return list of {@link BsonDocument}s which, when passed to {@link #gather}, combine to form the original document. * The main document, document corresponding to docRef, will be at the end of the list. * @see #gather */ - public List scatter(Reference docRef, BsonDocument document, Reference rootRef) { + public List scatter(Reference docRef, BsonDocument document) { List parts = new ArrayList<>(); for (GraftPoint graftPoint: graftPoints) { - scatterOneCollection(docRef, graftPoint, document, rootRef, parts); + scatterOneCollection(docRef, graftPoint, document, docRef.root(), parts); } - // `document` has now had the scattered pieces replaced by BsonBoolean.TRUE - String docBsonPath = "|" + String.join("|", docSegments(docRef, rootRef)); - parts.add(createRecipe(document, docBsonPath)); + // `document` has now had the scattered pieces stubbed-out by BsonBoolean.TRUE. + // Add the stubbed-out document as the final recipe in the parts list. + parts.add(createRecipe(document, BsonFormatter.docBsonPath(docRef, docRef.root()))); return parts; } - /** - * @return list of field names suitable for {@link #lookup} to find the document corresponding - * to docRef inside a document corresponding to rootRef - */ - static List docSegments(Reference docRef, Reference rootRef) { - ArrayList allSegments = dottedFieldNameSegments(docRef, docRef.path().length(), rootRef); - return allSegments - .subList(1, allSegments.size()); // Skip the "state" field - } - private void scatterOneCollection(Reference docRef, GraftPoint graftPoint, BsonDocument docToScatter, Reference rootRef, List parts) { // Only continue if the graft could point to a proper descendant node of docRef Path graftPath = graftPoint.entryPlaceholderRef.path(); @@ -164,11 +162,6 @@ private static List containerDocSegments(Reference docRef, int docRef return allSegments.subList(1, allSegments.size()); // Remove "state" segment } - /** - * entryPath and entryBsonPath must correspond to each other. - * They'll have the same segments, except where the BSON representation of a container actually contains its own - * fields (as with {@link SideTable}, in which case those fields will appear too. - */ private static BsonDocument createRecipe(BsonValue entryState, String bsonPathString) { return new BsonDocument() .append(BSON_PATH_FIELD, new BsonString(bsonPathString)) @@ -204,6 +197,11 @@ private static BsonDocument lookup(BsonDocument entireDoc, List segments * *

* partsList is a list of "instructions" for assembling a larger document. + * Each part contains a {@link DocumentFields#path path} field containing a {@link BsonFormatter#docBsonPath BSON path} + * that indicates where that part fits into the larger document; + * and a {@link DocumentFields#state state} field with the contents of the part. + * + *

* By design, this method is supposed to be simple and general; * any sophistication should be in {@link #scatter}. * This way, {@link #scatter} can evolve without breaking backward compatibility diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/FormatterTest.java b/bosk-bson/src/test/java/works/bosk/bson/BsonFormatterTest.java similarity index 64% rename from bosk-mongo/src/test/java/works/bosk/drivers/mongo/FormatterTest.java rename to bosk-bson/src/test/java/works/bosk/bson/BsonFormatterTest.java index 1bb2488e..1b65954d 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/FormatterTest.java +++ b/bosk-bson/src/test/java/works/bosk/bson/BsonFormatterTest.java @@ -1,16 +1,12 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import java.io.IOException; import java.util.ArrayList; -import java.util.stream.Stream; import org.bson.BsonDocument; import org.bson.BsonString; import org.bson.BsonValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import works.bosk.AbstractBoskTest; import works.bosk.Bosk; import works.bosk.Catalog; @@ -24,12 +20,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static works.bosk.TypeValidation.validateType; -class FormatterTest extends AbstractBoskTest { +class BsonFormatterTest extends AbstractBoskTest { Bosk bosk; CatalogReference entitiesRef; Reference weirdRef; static final String WEIRD_ID = "weird|i.d."; - Formatter formatter; + BsonFormatter formatter; private TestEntity weirdEntity; @BeforeEach @@ -41,7 +37,7 @@ void setupFormatter() throws InvalidTypeException, IOException, InterruptedExcep weirdEntity = builder.blankEntity(Identifier.from(WEIRD_ID), TestEnum.OK); bosk.driver().submitReplacement(entitiesRef, Catalog.of(weirdEntity)); bosk.driver().flush(); - formatter = new Formatter(bosk, new BsonPlugin()); + formatter = new BsonFormatter(bosk, new BsonPlugin()); } @Test @@ -74,46 +70,9 @@ void object2bsonValue() { ) ; - ArrayList dottedName = Formatter.dottedFieldNameSegments(weirdRef, weirdRef.path().length(), bosk.rootReference()); + ArrayList dottedName = BsonFormatter.dottedFieldNameSegments(weirdRef, weirdRef.path().length(), bosk.rootReference()); BsonDocument expected = new BsonDocument() .append(dottedName.get(dottedName.size()-1), weirdDoc); assertEquals(expected, actual); } - - @ParameterizedTest - @MethodSource("dottedNameCases") - void dottedFieldNameSegment(String plain, String dotted) { - assertEquals(dotted, Formatter.dottedFieldNameSegment(plain)); - } - - @ParameterizedTest - @MethodSource("dottedNameCases") - void undottedFieldNameSegment(String plain, String dotted) { - assertEquals(plain, Formatter.undottedFieldNameSegment(dotted)); - } - - static Stream dottedNameCases() { - return Stream.of( - dottedNameCase("%", "%25"), - dottedNameCase("$", "%24"), - dottedNameCase(".", "%2E"), - dottedNameCase("\0", "%00"), - dottedNameCase("|", "%7C"), - dottedNameCase("!", "%21"), - dottedNameCase("~", "%7E"), - dottedNameCase("[", "%5B"), - dottedNameCase("]", "%5D"), - dottedNameCase("+", "%2B"), - dottedNameCase(" ", "%20") - ); - } - - static Arguments dottedNameCase(String plain, String dotted) { - return Arguments.of(plain, dotted); - } - - @Test - void manifest_passesTypeValidation() throws InvalidTypeException { - validateType(Manifest.class); - } } diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonPluginTest.java b/bosk-bson/src/test/java/works/bosk/bson/BsonPluginTest.java similarity index 98% rename from bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonPluginTest.java rename to bosk-bson/src/test/java/works/bosk/bson/BsonPluginTest.java index bc882234..dd945177 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonPluginTest.java +++ b/bosk-bson/src/test/java/works/bosk/bson/BsonPluginTest.java @@ -1,4 +1,4 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import lombok.experimental.FieldNameConstants; import org.bson.BsonDocument; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java b/bosk-bson/src/test/java/works/bosk/bson/BsonSurgeonTest.java similarity index 94% rename from bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java rename to bosk-bson/src/test/java/works/bosk/bson/BsonSurgeonTest.java index 41a44e71..3bb11d41 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java +++ b/bosk-bson/src/test/java/works/bosk/bson/BsonSurgeonTest.java @@ -1,4 +1,4 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import java.io.IOException; import java.util.LinkedHashSet; @@ -28,33 +28,32 @@ import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static works.bosk.bson.BsonFormatter.docBsonPath; public class BsonSurgeonTest extends AbstractDriverTest { BsonSurgeon surgeon; BsonPlugin bsonPlugin; - Formatter formatter; + BsonFormatter formatter; private List>> graftPoints; Refs refs; public interface Refs { - @ReferencePath("/catalog") - CatalogReference catalog(); + @ReferencePath("/catalog") CatalogReference catalog(); @ReferencePath("/catalog/-entity-") Reference entity(Identifier entity); @ReferencePath("/catalog/-entity-/catalog") CatalogReference anyNestedCatalog(); @ReferencePath("/catalog/-entity-/catalog") CatalogReference nestedCatalog(Identifier entity); @ReferencePath("/catalog/-parent-/catalog/-child-") Reference child(Identifier parent, Identifier child); @ReferencePath("/catalog/-entity-/catalog/-child-/catalog") CatalogReference doubleNestedCatalog(); @ReferencePath("/catalog/-parent-/catalog/-child-/catalog/-grandchild-") Reference grandchild(Identifier parent, Identifier child, Identifier grandchild); - @ReferencePath("/sideTable") - SideTableReference sideTable(); + @ReferencePath("/sideTable") SideTableReference sideTable(); } @BeforeEach void setup() throws InvalidTypeException, IOException, InterruptedException { setupBosksAndReferences(Bosk.simpleDriver()); bsonPlugin = new BsonPlugin(); - formatter = new Formatter(bosk, bsonPlugin); + formatter = new BsonFormatter(bosk, bsonPlugin); refs = bosk.buildReferences(Refs.class); @@ -124,7 +123,7 @@ void root_partForEachEntry() { entireDoc = (BsonDocument) formatter.object2bsonValue(rootRef.value(), rootRef.targetType()); } - List parts = surgeon.scatter(rootRef, entireDoc.clone(), bosk.rootReference()); + List parts = surgeon.scatter(rootRef, entireDoc.clone()); List partPaths = parts.stream() .map(part -> part.getString("_id")) .map(BsonString::getValue) @@ -194,9 +193,9 @@ private void assertRoundTripWorks(Reference mainRef) { entireDoc = (BsonDocument) formatter.object2bsonValue(mainRef.value(), mainRef.targetType()); } - List parts = surgeon.scatter(mainRef, entireDoc.clone(), bosk.rootReference()); + List parts = surgeon.scatter(mainRef, entireDoc.clone()); - BsonString mainPath = new BsonString("|" + String.join("|", BsonSurgeon.docSegments(mainRef, bosk.rootReference()))); + BsonString mainPath = new BsonString(docBsonPath(mainRef, bosk.rootReference())); assertEquals(mainPath, parts.get(parts.size()-1).getString("_id"), "Last part must correspond to the main doc"); diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverDottedFieldNameTest.java b/bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest.java similarity index 92% rename from bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverDottedFieldNameTest.java rename to bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest.java index c4d2255b..28fc0733 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverDottedFieldNameTest.java +++ b/bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest.java @@ -1,4 +1,4 @@ -package works.bosk.drivers.mongo; +package works.bosk.bson; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -19,7 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static works.bosk.BoskTestUtils.boskName; -class MongoDriverDottedFieldNameTest extends AbstractDriverTest { +class DottedFieldNameTest { private Bosk bosk; @BeforeEach @@ -58,7 +58,7 @@ private Arguments args(String boskPath, String dottedFieldName) { @ArgumentsSource(PathArgumentProvider.class) void testDottedFieldNameOf(String boskPath, String dottedFieldName) throws InvalidTypeException { Reference reference = bosk.rootReference().then(Object.class, Path.parse(boskPath)); - String actual = Formatter.dottedFieldNameOf(reference, bosk.rootReference()); + String actual = BsonFormatter.dottedFieldNameOf(reference, bosk.rootReference()); assertEquals(dottedFieldName, actual); //assertThrows(AssertionError.class, ()-> MongoDriver.dottedFieldNameOf(reference, catalogReference.then(Identifier.from("whoopsie")))); } @@ -67,7 +67,7 @@ void testDottedFieldNameOf(String boskPath, String dottedFieldName) throws Inval @ArgumentsSource(PathArgumentProvider.class) void testReferenceTo(String boskPath, String dottedFieldName) throws InvalidTypeException { Reference expected = bosk.rootReference().then(Object.class, Path.parse(boskPath)); - Reference actual = Formatter.referenceTo(dottedFieldName, bosk.rootReference()); + Reference actual = BsonFormatter.referenceTo(dottedFieldName, bosk.rootReference()); assertEquals(expected, actual); assertEquals(expected.path(), actual.path()); assertEquals(expected.targetType(), actual.targetType()); @@ -96,7 +96,7 @@ void testTruncatedPaths() throws InvalidTypeException { private String dotted(String path, int pathLength) throws InvalidTypeException { Reference reference = bosk.rootReference().then(Object.class, Path.parseParameterized(path)); - return Formatter.dottedFieldNameOf(reference, pathLength, bosk.rootReference()); + return BsonFormatter.dottedFieldNameOf(reference, pathLength, bosk.rootReference()); } } diff --git a/bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest2.java b/bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest2.java new file mode 100644 index 00000000..9f601f6a --- /dev/null +++ b/bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest2.java @@ -0,0 +1,44 @@ +package works.bosk.bson; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.*; + +class DottedFieldNameTest2 { + + @ParameterizedTest + @MethodSource("dottedNameCases") + void dottedFieldNameSegment(String plain, String dotted) { + assertEquals(dotted, BsonFormatter.dottedFieldNameSegment(plain)); + } + + @ParameterizedTest + @MethodSource("dottedNameCases") + void undottedFieldNameSegment(String plain, String dotted) { + assertEquals(plain, BsonFormatter.undottedFieldNameSegment(dotted)); + } + + static Stream dottedNameCases() { + return Stream.of( + dottedNameCase("%", "%25"), + dottedNameCase("$", "%24"), + dottedNameCase(".", "%2E"), + dottedNameCase("\0", "%00"), + dottedNameCase("|", "%7C"), + dottedNameCase("!", "%21"), + dottedNameCase("~", "%7E"), + dottedNameCase("[", "%5B"), + dottedNameCase("]", "%5D"), + dottedNameCase("+", "%2B"), + dottedNameCase(" ", "%20") + ); + } + + static Arguments dottedNameCase(String plain, String dotted) { + return Arguments.of(plain, dotted); + } + +} diff --git a/bosk-core/src/main/java/works/bosk/Path.java b/bosk-core/src/main/java/works/bosk/Path.java index 247116a6..c8dd0afa 100644 --- a/bosk-core/src/main/java/works/bosk/Path.java +++ b/bosk-core/src/main/java/works/bosk/Path.java @@ -198,6 +198,9 @@ public static String validSegment(String str) { } } + /** + * @return true if {@code other} equals {@codee this} or can be truncated to equal {@code this}. + */ public final boolean isPrefixOf(Path other) { int excessSegments = other.length() - this.length(); if (excessSegments >= 0) { diff --git a/bosk-core/src/main/java/works/bosk/Reference.java b/bosk-core/src/main/java/works/bosk/Reference.java index 22a8adca..7fece722 100644 --- a/bosk-core/src/main/java/works/bosk/Reference.java +++ b/bosk-core/src/main/java/works/bosk/Reference.java @@ -187,6 +187,7 @@ default BindingEnvironment parametersFrom(Path definitePath) { /** * @return this.path().{@link Path#isPrefixOf isPrefixOf}(other.path()) + * @see Path#isPrefixOf */ default boolean encloses(Reference other) { return this.path().isPrefixOf(other.path()); diff --git a/bosk-mongo/build.gradle b/bosk-mongo/build.gradle index 5ecc51c1..46f5c6fa 100644 --- a/bosk-mongo/build.gradle +++ b/bosk-mongo/build.gradle @@ -26,6 +26,7 @@ compileTestJava { dependencies { api project(":bosk-core") + implementation project(":bosk-bson") api 'org.mongodb:mongodb-driver-sync:5.1.2' implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' // To stop warnings about When from MongoDB driver diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/Formatter.java b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/Formatter.java index 9d697605..5ca96a5e 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/Formatter.java +++ b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/Formatter.java @@ -3,49 +3,32 @@ import com.mongodb.client.model.changestream.ChangeStreamDocument; import com.mongodb.client.model.changestream.UpdateDescription; import java.lang.reflect.Type; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Set; -import java.util.function.Function; -import java.util.function.UnaryOperator; -import java.util.regex.Pattern; import lombok.NonNull; import org.bson.BsonBinaryWriter; import org.bson.BsonDocument; import org.bson.BsonDocumentReader; -import org.bson.BsonDocumentWriter; import org.bson.BsonInt32; import org.bson.BsonInt64; -import org.bson.BsonReader; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; -import org.bson.codecs.DocumentCodecProvider; import org.bson.codecs.EncoderContext; -import org.bson.codecs.ValueCodecProvider; -import org.bson.codecs.configuration.CodecRegistries; -import org.bson.codecs.configuration.CodecRegistry; import org.bson.io.BasicOutputBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import works.bosk.BoskInfo; -import works.bosk.Listing; import works.bosk.MapValue; import works.bosk.Reference; import works.bosk.SerializationPlugin; -import works.bosk.SideTable; -import works.bosk.exceptions.InvalidTypeException; +import works.bosk.bson.BsonPlugin; +import works.bosk.bson.BsonFormatter; -import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static works.bosk.ReferenceUtils.rawClass; @@ -55,10 +38,7 @@ * * @author pdoyle */ -final class Formatter { - private final CodecRegistry simpleCodecs; - private final Function> preferredBoskCodecs; - private final Function, SerializationPlugin.DeserializationScope> deserializationScopeFunction; +public final class Formatter extends BsonFormatter { /** * If the diagnostic attributes are identical from one update to the next, @@ -67,13 +47,8 @@ final class Formatter { */ private volatile MapValue lastEventDiagnosticAttributes = MapValue.empty(); - Formatter(BoskInfo boskInfo, BsonPlugin bsonPlugin) { - this.simpleCodecs = CodecRegistries.fromProviders( - bsonPlugin.codecProviderFor(boskInfo), - new ValueCodecProvider(), - new DocumentCodecProvider()); - this.preferredBoskCodecs = type -> bsonPlugin.getCodec(type, rawClass(type), simpleCodecs, boskInfo); - this.deserializationScopeFunction = bsonPlugin::newDeserializationScope; + public Formatter(BoskInfo boskInfo, BsonPlugin bsonPlugin) { + super(boskInfo, bsonPlugin); } /** @@ -99,44 +74,18 @@ final class Formatter { */ static final BsonInt64 REVISION_ONE = new BsonInt64(1); - /** - * The fields of the main MongoDB document. Case-sensitive. - * - *

- * No field name should be a prefix of any other. - */ - enum DocumentFields { - path, - state, - revision, - diagnostics, - } - private final BsonInt32 SUPPORTED_MANIFEST_VERSION = new BsonInt32(1); // // Helpers to translate Bosk <-> MongoDB // - Codec codecFor(Type type) { - // BsonPlugin gives better codecs than CodecRegistry, because BsonPlugin is aware of generics, - // so we always try that first. The CodecSupplier protocol uses "null" to indicate that another - // CodecSupplier should be used, so we follow that protocol and fall back on the CodecRegistry. - // TODO: Should this logic be in BsonPlugin? It has nothing to do with MongoDriver really. - Codec result = preferredBoskCodecs.apply(type); - if (result == null) { - return simpleCodecs.get(rawClass(type)); - } else { - return result; - } - } - @SuppressWarnings("unchecked") T document2object(BsonDocument doc, Reference target) { Type type = target.targetType(); Class objectClass = (Class) rawClass(type); Codec objectCodec = (Codec) codecFor(type); - try (@SuppressWarnings("unused") BsonPlugin.DeserializationScope scope = deserializationScopeFunction.apply(target)) { + try (@SuppressWarnings("unused") SerializationPlugin.DeserializationScope scope = deserializationScopeFunction.apply(target)) { return objectClass.cast(objectCodec.decode(doc.asBsonReader(), DecoderContext.builder().build())); } } @@ -202,42 +151,6 @@ MapValue decodeDiagnosticAttributes(BsonDocument diagnostics) { return result; } - /** - * @see #bsonValue2object(BsonValue, Reference) - */ - @SuppressWarnings("unchecked") - BsonValue object2bsonValue(T object, Type type) { - rawClass(type).cast(object); - Codec objectCodec = (Codec) codecFor(type); - BsonDocument document = new BsonDocument(); - try (BsonDocumentWriter writer = new BsonDocumentWriter(document)) { - // To support arbitrary values, not just whole documents, we put the result INSIDE a document. - writer.writeStartDocument(); - writer.writeName("value"); - objectCodec.encode(writer, object, EncoderContext.builder().build()); - writer.writeEndDocument(); - } - return document.get("value"); - } - - /** - * @see #object2bsonValue(Object, Type) - */ - @SuppressWarnings("unchecked") - T bsonValue2object(BsonValue bson, Reference target) { - Codec objectCodec = (Codec) codecFor(target.targetType()); - BsonDocument document = new BsonDocument(); - document.append("value", bson); - try ( - @SuppressWarnings("unused") BsonPlugin.DeserializationScope scope = deserializationScopeFunction.apply(target); - BsonReader reader = document.asBsonReader() - ) { - reader.readStartDocument(); - reader.readName("value"); - return objectCodec.decode(reader, DecoderContext.builder().build()); - } - } - @SuppressWarnings("unchecked") long bsonValueBinarySize(BsonValue bson) { Codec codec = new BsonValueCodec(); @@ -312,161 +225,5 @@ static BsonDocument getDiagnosticAttributesIfAny(BsonDocument fullDocument) { } } - /** - * @return MongoDB field name corresponding to the given Reference - * @see #referenceTo(String, Reference) - */ - static String dottedFieldNameOf(Reference ref, Reference startingRef) { - return dottedFieldNameOf(ref, ref.path().length(), startingRef); - } - - /** - * @param refLength behave as though ref were truncated to this length, without actually having to do it - * @return MongoDB field name corresponding to the given Reference - * @see #referenceTo(String, Reference) - */ - static String dottedFieldNameOf(Reference ref, int refLength, Reference startingRef) { - ArrayList segments = dottedFieldNameSegments(ref, refLength, startingRef); - return String.join(".", segments.toArray(new String[0])); - } - - static ArrayList dottedFieldNameSegments(Reference ref, int refLength, Reference startingRef) { - assert startingRef.path().matchesPrefixOf(ref.path()): "'" + ref + "' must be under '" + startingRef + "'"; - ArrayList segments = new ArrayList<>(); - segments.add(DocumentFields.state.name()); - buildDottedFieldNameOf(ref, startingRef.path().length(), refLength, segments); - return segments; - } - - /** - * @param elementRefLength behave as though elementRef were truncated to this length, without actually having to do it - * @return MongoDB field name corresponding to the container (Catalog or SideTable) that contains the given element - * @see #referenceTo(String, Reference) - */ - static List containerSegments(Reference elementRef, int elementRefLength, Reference startingRef) { - List elementSegments = dottedFieldNameSegments(elementRef, elementRefLength, startingRef); - return elementSegments.subList(0, elementSegments.size()-1); // Trim off the element itself - } - - /** - * @param refLength behave as though ref were truncated to this length without actually having to do it - */ - private static void buildDottedFieldNameOf(Reference ref, int startLength, int refLength, ArrayList segments) { - if (ref.path().length() > startLength) { - Reference enclosingReference = ref.enclosingReference(Object.class); - buildDottedFieldNameOf(enclosingReference, startLength, refLength, segments); - if (ref.path().length() <= refLength) { - if (Listing.class.isAssignableFrom(enclosingReference.targetClass())) { - segments.add("ids"); - } else if (SideTable.class.isAssignableFrom(enclosingReference.targetClass())) { - segments.add("valuesById"); - } - segments.add(dottedFieldNameSegment(ref.path().lastSegment())); - } - } - } - - static String dottedFieldNameSegment(String segment) { - return ENCODER.apply(segment); - } - - static String undottedFieldNameSegment(String dottedSegment) { - return DECODER.apply(dottedSegment); - } - - /** - * @return Reference corresponding to the given field name - * @see #dottedFieldNameOf - */ - @SuppressWarnings("unchecked") - static Reference referenceTo(String dottedName, Reference startingReference) throws InvalidTypeException { - Reference ref = startingReference; - Iterator iter = Arrays.asList(dottedName.split(Pattern.quote("."))).iterator(); - skipField(ref, iter, DocumentFields.state.name()); // The entire Bosk state is in this field - while (iter.hasNext()) { - if (Listing.class.isAssignableFrom(ref.targetClass())) { - skipField(ref, iter, "ids"); - } else if (SideTable.class.isAssignableFrom(ref.targetClass())) { - skipField(ref, iter, "valuesById"); - } - if (iter.hasNext()) { - String segment = undottedFieldNameSegment(iter.next()); - ref = ref.then(Object.class, segment); - } - } - return (Reference) ref; - } - - private static void skipField(Reference ref, Iterator iter, String expectedName) { - String actualName; - try { - actualName = iter.next(); - } catch (NoSuchElementException e) { - throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; encountered end of dotted field name"); - } - if (!expectedName.equals(actualName)) { - throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; was: " + actualName); - } - } - - private static final UnaryOperator DECODER; - private static final UnaryOperator ENCODER; - - static { - DECODER = s->{ - return URLDecoder.decode(s, StandardCharsets.UTF_8); - }; - - ENCODER = s->{ - // Selective percent-encoding of characters MongoDB doesn't like. - // Standard percent-encoding doesn't handle the period character, which - // we want, so if we're already diverging from the standard, we might - // as well do something that suits our needs. - // Good to stay compatible with standard percent-DEcoding, though. - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < s.length(); ) { - int cp = s.codePointAt(i); - switch (cp) { - case '%': // For percent-encoding - case '+': case ' ': // These two are affected by URLDecoder - case '$': // MongoDB treats these specially - case '.': // MongoDB separator for dotted field names - case 0: // Can MongoDB handle nulls? Probably. Do we want to find out? Not really. - case '|': // (These are reserved for internal use) - case '!': - case '~': - case '[': - case ']': - appendPercentEncoded(sb, cp); - break; - default: - sb.appendCodePoint(cp); - break; - } - i += Character.charCount(cp); - } - return sb.toString(); - }; - } - - private static void appendPercentEncoded(StringBuilder sb, int cp) { - assert 0 <= cp && cp <= 255; - sb - .append('%') - .append(hexCharForDigit(cp / 16)) - .append(hexCharForDigit(cp % 16)); - } - - /** - * An uppercase version of {@link Character#forDigit} with a radix of 16. - */ - private static char hexCharForDigit(int value) { - if (value < 10) { - return (char)('0' + value); - } else { - return (char)('A' + value - 10); - } - } - private static final Logger LOGGER = LoggerFactory.getLogger(Formatter.class); } diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MainDriver.java b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MainDriver.java index a1844e59..2098b4ff 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MainDriver.java +++ b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MainDriver.java @@ -29,7 +29,8 @@ import works.bosk.Identifier; import works.bosk.Reference; import works.bosk.StateTreeNode; -import works.bosk.drivers.mongo.Formatter.DocumentFields; +import works.bosk.bson.BsonPlugin; +import works.bosk.bson.BsonFormatter.DocumentFields; import works.bosk.drivers.mongo.MappedDiagnosticContext.MDCScope; import works.bosk.drivers.mongo.MongoDriverSettings.DatabaseFormat; import works.bosk.drivers.mongo.MongoDriverSettings.InitialDatabaseUnavailableMode; diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MongoDriver.java b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MongoDriver.java index 005ee2ed..1a5476bb 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MongoDriver.java +++ b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/MongoDriver.java @@ -7,6 +7,7 @@ import works.bosk.BoskInfo; import works.bosk.DriverFactory; import works.bosk.StateTreeNode; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.mongo.status.MongoStatus; /** diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/PandoFormatDriver.java b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/PandoFormatDriver.java index d2f6eca5..9a2f4c08 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/PandoFormatDriver.java +++ b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/PandoFormatDriver.java @@ -40,6 +40,9 @@ import works.bosk.Reference; import works.bosk.RootReference; import works.bosk.StateTreeNode; +import works.bosk.bson.BsonFormatter; +import works.bosk.bson.BsonPlugin; +import works.bosk.bson.BsonSurgeon; import works.bosk.exceptions.FlushFailureException; import works.bosk.exceptions.InvalidTypeException; import works.bosk.exceptions.NotYetImplementedException; @@ -57,6 +60,7 @@ import static java.util.stream.Collectors.toList; import static org.bson.BsonBoolean.TRUE; import static works.bosk.Path.parseParameterized; +import static works.bosk.bson.BsonFormatter.docBsonPath; import static works.bosk.util.Classes.enumerableByIdentifier; /** @@ -71,6 +75,7 @@ final class PandoFormatDriver extends AbstractFormatDri private final FlushLock flushLock; private final BsonSurgeon bsonSurgeon; private final Demultiplexer demultiplexer = new Demultiplexer(); + private final List>> graftPoints; private volatile BsonInt64 revisionToSkip = null; @@ -91,12 +96,11 @@ final class PandoFormatDriver extends AbstractFormatDri this.collection = collection; this.downstream = downstream; this.flushLock = flushLock; - - this.bsonSurgeon = new BsonSurgeon( - format.graftPoints().stream() + this.graftPoints = format.graftPoints().stream() .map(s -> referenceTo(s, rootRef)) .sorted(comparing((Reference ref) -> ref.path().length()).reversed()) - .collect(toList())); + .collect(toList()); + this.bsonSurgeon = new BsonSurgeon(graftPoints); } private static Reference> referenceTo(String pathString, RootReference rootRef) { @@ -117,7 +121,7 @@ public void submitInitialization(Reference target, T newValue) { collection.ensureTransactionStarted(); Reference mainRef = mainRef(target); BsonDocument filter = documentFilter(mainRef) - .append(Formatter.dottedFieldNameOf(target, mainRef), new BsonDocument("$exists", TRUE)); + .append(BsonFormatter.dottedFieldNameOf(target, mainRef), new BsonDocument("$exists", TRUE)); if (documentExists(filter)) { LOGGER.debug("Already exists: {}", filter); collection.abortTransaction(); @@ -425,7 +429,7 @@ private Reference documentID2MainRef(String pipedPath, ChangeStreamDocument void doReplacement(Reference target, T newValue) { if (rootRef.equals(mainRef)) { LOGGER.debug("| Root ref is main ref"); LOGGER.debug("| Pre-delete on root document"); - String key = Formatter.dottedFieldNameOf(target, rootRef); + String key = BsonFormatter.dottedFieldNameOf(target, rootRef); LOGGER.debug("| Pre-delete field {}", key); doUpdate( // Important: don't bump the revision field because that's how we identify the last event in a transaction new BsonDocument("$unset", new BsonDocument(key, BsonNull.VALUE)), @@ -564,7 +568,7 @@ private void doReplacement(Reference target, T newValue) { } // Update part of the main doc (which must already exist) - String key = Formatter.dottedFieldNameOf(target, mainRef); + String key = BsonFormatter.dottedFieldNameOf(target, mainRef); LOGGER.debug("| Pre-delete field {} in {}", key, mainRef); BsonDocument preDelete = new BsonDocument("$unset", new BsonDocument(key, BsonNull.VALUE)); doUpdate(preDelete, standardPreconditions(target, mainRef, filter)); @@ -608,7 +612,7 @@ private void doDelete(Reference target) { private boolean preconditionFailed(Reference precondition, Identifier requiredValue) { Reference mainRef = mainRef(precondition); BsonDocument filter = documentFilter(mainRef) - .append(Formatter.dottedFieldNameOf(precondition, mainRef), new BsonString(requiredValue.toString())); + .append(BsonFormatter.dottedFieldNameOf(precondition, mainRef), new BsonString(requiredValue.toString())); LOGGER.debug("Precondition filter: {}", filter); boolean result = !documentExists(filter); if (result) { @@ -634,8 +638,7 @@ private Reference mainRef(Reference target) { // graftPoints is in descending order of depth. // TODO: This could be done more efficiently, perhaps using a trie int targetPathLength = target.path().length(); - for (BsonSurgeon.GraftPoint graftPoint: bsonSurgeon.graftPoints) { - Reference candidateContainer = graftPoint.containerRef(); + for (var candidateContainer: graftPoints) { int containerPathLength = candidateContainer.path().length(); if (containerPathLength <= targetPathLength - 1) { if (candidateContainer.path().matchesPrefixOf(target.path())) { @@ -695,8 +698,7 @@ private BsonDocument rootDocumentFilter() { } private BsonDocument documentFilter(Reference docRef) { - String id = '|' + String.join("|", BsonSurgeon.docSegments(docRef, rootRef)); - return new BsonDocument("_id", new BsonString(id)); + return new BsonDocument("_id", new BsonString(docBsonPath(docRef, rootRef))); } private BsonDocument standardRootPreconditions(Reference target) { @@ -705,7 +707,7 @@ private BsonDocument standardRootPreconditions(Reference target) { private BsonDocument standardPreconditions(Reference target, Reference startingRef, BsonDocument filter) { if (!target.path().equals(startingRef.path())) { - String enclosingObjectKey = Formatter.dottedFieldNameOf(target.enclosingReference(Object.class), startingRef); + String enclosingObjectKey = BsonFormatter.dottedFieldNameOf(target.enclosingReference(Object.class), startingRef); BsonDocument condition = new BsonDocument("$type", new BsonString("object")); filter.put(enclosingObjectKey, condition); LOGGER.debug("| Precondition: {} {}", enclosingObjectKey, condition); @@ -716,12 +718,12 @@ private BsonDocument standardPreconditions(Reference target, Reference private BsonDocument explicitPreconditions(Reference target, Reference preconditionRef, Identifier requiredValue) { BsonDocument filter = standardRootPreconditions(target); BsonDocument precondition = new BsonDocument("$eq", new BsonString(requiredValue.toString())); - filter.put(Formatter.dottedFieldNameOf(preconditionRef, rootRef), precondition); + filter.put(BsonFormatter.dottedFieldNameOf(preconditionRef, rootRef), precondition); return filter; } private BsonDocument replacementDoc(Reference target, BsonValue value, Reference startingRef) { - String key = Formatter.dottedFieldNameOf(target, startingRef); + String key = BsonFormatter.dottedFieldNameOf(target, startingRef); LOGGER.debug("| Set field {}: {}", key, value); BsonDocument result = blankUpdateDoc(); result.compute("$set", (__,existing) -> { @@ -735,7 +737,7 @@ private BsonDocument replacementDoc(Reference target, BsonValue value, Re } private BsonDocument deletionDoc(Reference target, Reference startingRef) { - String key = Formatter.dottedFieldNameOf(target, startingRef); + String key = BsonFormatter.dottedFieldNameOf(target, startingRef); LOGGER.debug("| Unset field {}", key); return blankUpdateDoc().append("$unset", new BsonDocument(key, BsonNull.VALUE)); // Value is ignored } @@ -794,7 +796,7 @@ private void replaceUpdatedFields(Reference mainRef, @Nullable BsonDocument u if (dottedName.startsWith(Formatter.DocumentFields.state.name())) { Reference ref; try { - ref = Formatter.referenceTo(dottedName, mainRef); + ref = BsonFormatter.referenceTo(dottedName, mainRef); } catch (InvalidTypeException e) { logNonexistentField(dottedName, e); continue; @@ -809,7 +811,7 @@ private void replaceUpdatedFields(Reference mainRef, @Nullable BsonDocument u BsonValue replacementValue = entry.getValue(); if (replacementValue instanceof BsonDocument) { LOGGER.debug("Replacement value is a document; gather along with {} subparts", subParts.size()); - String mainID = "|" + String.join("|", BsonSurgeon.docSegments(ref, mainRef)); + String mainID = docBsonPath(ref, mainRef); BsonDocument mainDocument = new BsonDocument() .append("_id", new BsonString(mainID)) .append("state", replacementValue); @@ -851,7 +853,7 @@ private void deleteRemovedFields(Reference mainRef, @Nullable List re if (dottedName.startsWith(Formatter.DocumentFields.state.name())) { Reference ref; try { - ref = Formatter.referenceTo(dottedName, mainRef); + ref = BsonFormatter.referenceTo(dottedName, mainRef); } catch (InvalidTypeException e) { logNonexistentField(dottedName, e); continue; @@ -876,7 +878,7 @@ private void deletePartsUnder(Reference target) { if (mainRef.path().isEmpty()) { prefix = "|"; } else { - prefix = "|" + String.join("|", BsonSurgeon.docSegments(mainRef, rootRef)) + "|"; + prefix = docBsonPath(mainRef, rootRef) + "|"; } // Every doc whose ID starts with the prefix and has at least one more character @@ -897,7 +899,7 @@ private void deletePartsUnder(Reference target) { * (which is not the root of the bosk state tree, unless of course target is the root reference) */ private BsonDocument upsertAndRemoveSubParts(Reference target, BsonDocument value) { - List allParts = bsonSurgeon.scatter(target, value, rootRef); + List allParts = bsonSurgeon.scatter(target, value); // NOTE: `value` has now been mutated so the parts have been stubbed out List subParts = allParts.subList(0, allParts.size() - 1); diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/SequoiaFormatDriver.java b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/SequoiaFormatDriver.java index eb992f2b..9453d511 100644 --- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/SequoiaFormatDriver.java +++ b/bosk-mongo/src/main/java/works/bosk/drivers/mongo/SequoiaFormatDriver.java @@ -28,7 +28,8 @@ import works.bosk.MapValue; import works.bosk.Reference; import works.bosk.StateTreeNode; -import works.bosk.drivers.mongo.Formatter.DocumentFields; +import works.bosk.bson.BsonPlugin; +import works.bosk.bson.BsonFormatter.DocumentFields; import works.bosk.exceptions.FlushFailureException; import works.bosk.exceptions.InvalidTypeException; @@ -42,7 +43,7 @@ import static org.bson.BsonBoolean.FALSE; import static works.bosk.drivers.mongo.Formatter.REVISION_ZERO; import static works.bosk.drivers.mongo.Formatter.dottedFieldNameOf; -import static works.bosk.drivers.mongo.Formatter.referenceTo; +import static works.bosk.bson.BsonFormatter.referenceTo; import static works.bosk.drivers.mongo.MainDriver.MANIFEST_ID; import static works.bosk.drivers.mongo.MongoDriverSettings.ManifestMode.CREATE_IF_ABSENT; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/AbstractMongoDriverTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/AbstractMongoDriverTest.java index 5f039fcb..9643119a 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/AbstractMongoDriverTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/AbstractMongoDriverTest.java @@ -26,6 +26,7 @@ import works.bosk.Reference; import works.bosk.SideTable; import works.bosk.annotations.ReferencePath; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.mongo.MongoDriverSettings.MongoDriverSettingsBuilder; import works.bosk.drivers.state.TestEntity; import works.bosk.drivers.state.TestValues; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MiscMongoDriverTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MiscMongoDriverTest.java new file mode 100644 index 00000000..ff2f7258 --- /dev/null +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MiscMongoDriverTest.java @@ -0,0 +1,14 @@ +package works.bosk.drivers.mongo; + +import org.junit.jupiter.api.Test; +import works.bosk.exceptions.InvalidTypeException; + +import static works.bosk.TypeValidation.validateType; + +public class MiscMongoDriverTest { + @Test + void manifest_passesTypeValidation() throws InvalidTypeException { + validateType(Manifest.class); + } + +} diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverConformanceTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverConformanceTest.java index a65af742..070d1d20 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverConformanceTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverConformanceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach; import works.bosk.DriverFactory; import works.bosk.StateTreeNode; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.DriverConformanceTest; import works.bosk.drivers.mongo.TestParameters.EventTiming; import works.bosk.drivers.mongo.TestParameters.ParameterSet; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverHanoiTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverHanoiTest.java index c3130a93..f83865ea 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverHanoiTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverHanoiTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import works.bosk.DriverStack; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.HanoiTest; import works.bosk.junit.ParametersByName; import works.bosk.junit.Slow; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java index abde3d4b..c06212dc 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java @@ -30,6 +30,7 @@ import works.bosk.Reference; import works.bosk.SideTable; import works.bosk.annotations.Polyfill; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.BufferingDriver; import works.bosk.drivers.state.TestEntity; import works.bosk.drivers.state.TestValues; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java index 7fff2c6f..6606c1ed 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,10 +15,12 @@ import works.bosk.Reference; import works.bosk.annotations.ReferencePath; import works.bosk.drivers.state.TestEntity; +import works.bosk.exceptions.InvalidTypeException; import works.bosk.junit.ParametersByName; import static org.junit.jupiter.api.Assertions.assertEquals; import static works.bosk.BoskTestUtils.boskName; +import static works.bosk.TypeValidation.validateType; @UsesMongoService public class SchemaEvolutionTest { diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/example/ExampleBosk.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/example/ExampleBosk.java index 5a311503..40c14491 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/example/ExampleBosk.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/example/ExampleBosk.java @@ -5,7 +5,7 @@ import works.bosk.DriverFactory; import works.bosk.Reference; import works.bosk.annotations.ReferencePath; -import works.bosk.drivers.mongo.BsonPlugin; +import works.bosk.bson.BsonPlugin; import works.bosk.drivers.mongo.MongoDriver; import works.bosk.drivers.mongo.MongoDriverSettings; import works.bosk.exceptions.InvalidTypeException; diff --git a/lib-testing/build.gradle b/lib-testing/build.gradle index 6651c428..b57d5549 100644 --- a/lib-testing/build.gradle +++ b/lib-testing/build.gradle @@ -18,7 +18,7 @@ dependencies { implementation project(":bosk-testing") // These are for AbstractRoundTripTest. That logic ought to be moved to their respective sub-projects implementation project(":bosk-jackson") - implementation project(":bosk-mongo") + implementation project(":bosk-bson") // The bosk.development plugin brings these in as test dependencies, // but we need them as main dependencies. diff --git a/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java b/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java index 2755d309..e1f98593 100644 --- a/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java +++ b/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import java.io.IOException; -import java.lang.reflect.Parameter; import java.lang.reflect.RecordComponent; import java.lang.reflect.Type; import java.util.IdentityHashMap; @@ -29,7 +28,7 @@ import org.bson.codecs.configuration.CodecRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import works.bosk.drivers.mongo.BsonPlugin; +import works.bosk.bson.BsonPlugin; import works.bosk.exceptions.InvalidTypeException; import works.bosk.jackson.JacksonPlugin; import works.bosk.jackson.JacksonPluginConfiguration; diff --git a/settings.gradle b/settings.gradle index 0fd8d717..e029e397 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'bosk' include 'bosk-annotations' +include 'bosk-bson' include 'bosk-core' include 'bosk-jackson' include 'bosk-logback'