Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
Refactor fixture creation for easier readability. Tweak documentation wording.

See #4571
Original pull request: #4574
  • Loading branch information
mp911de committed Nov 30, 2023
1 parent 97433d0 commit 4aec4f3
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

Expand Down Expand Up @@ -677,15 +678,144 @@ void readsListOfMapsCorrectly() {
assertThat(wrapper.listOfMaps.get(0).get("Foo")).isEqualTo(Locale.ENGLISH);
}

@ParameterizedTest // GH-4571
@ParameterizedTest(name = "{4}") // GH-4571
@MethodSource("listMapSetReadingSource")
<T> void initializesListMapSetPropertiesIfRequiredOnRead(org.bson.Document source, Class<T> type,
Function<T, Object> valueFunction, Object expectedValue) {
Function<T, Object> valueFunction, Object expectedValue, String displayName) {

T target = converter.read(type, source);
assertThat(target).extracting(valueFunction).isEqualTo(expectedValue);
}

private static Stream<Arguments> listMapSetReadingSource() {

Stream<Arguments> initialList = fixtureFor("contacts", CollectionWrapper.class, CollectionWrapper::getContacts,
builder -> {

builder.onValue(Collections.emptyList()).expect(Collections.emptyList());
builder.onNull().expect(null);
builder.onEmpty().expect(null);
});

Stream<Arguments> initializedList = fixtureFor("autoInitList", CollectionWrapper.class,
CollectionWrapper::getAutoInitList, builder -> {

builder.onValue(Collections.emptyList()).expect(Collections.emptyList());
builder.onNull().expect(null);
builder.onEmpty().expect(Collections.singletonList("spring"));
});

Stream<Arguments> initialSet = fixtureFor("contactsSet", CollectionWrapper.class, CollectionWrapper::getContactsSet,
builder -> {

builder.onValue(Collections.emptyList()).expect(Collections.emptySet());
builder.onNull().expect(null);
builder.onEmpty().expect(null);
});

Stream<Arguments> initialMap = fixtureFor("map", ClassWithMapProperty.class, ClassWithMapProperty::getMap,
builder -> {

builder.onValue(new org.bson.Document()).expect(Collections.emptyMap());
builder.onNull().expect(null);
builder.onEmpty().expect(null);
});

Stream<Arguments> initializedMap = fixtureFor("autoInitMap", ClassWithMapProperty.class,
ClassWithMapProperty::getAutoInitMap, builder -> {

builder.onValue(new org.bson.Document()).expect(Collections.emptyMap());
builder.onNull().expect(null);
builder.onEmpty().expect(Collections.singletonMap("spring", "data"));
});

return Stream.of(initialList, initializedList, initialSet, initialMap, initializedMap).flatMap(Function.identity());
}

static <T> Stream<Arguments> fixtureFor(String field, Class<T> type, Function<T, Object> valueFunction,
Consumer<FixtureBuilder> builderConsumer) {

FixtureBuilder builder = new FixtureBuilder(field, type, valueFunction);

builderConsumer.accept(builder);

return builder.fixtures.stream();
}

/**
* Builder for fixtures.
*/
static class FixtureBuilder {

private final String field;
private final Class<?> typeUnderTest;
private final Function<?, Object> valueMappingFunction;
final List<Arguments> fixtures = new ArrayList<>();

FixtureBuilder(String field, Class<?> typeUnderTest, Function<?, Object> valueMappingFunction) {
this.field = field;
this.typeUnderTest = typeUnderTest;
this.valueMappingFunction = valueMappingFunction;
}

/**
* If the document value is {@code null}.
*/
FixtureStep onNull() {
return new FixtureStep(false, null);
}

/**
* If the document value is {@code value}.
*/
FixtureStep onValue(@Nullable Object value) {
return new FixtureStep(false, value);
}

/**
* If the document does not contain the field.
*/
FixtureStep onEmpty() {
return new FixtureStep(true, null);
}

class FixtureStep {

private final boolean empty;
private final @Nullable Object documentValue;

public FixtureStep(boolean empty, @Nullable Object documentValue) {
this.empty = empty;
this.documentValue = documentValue;
}

/**
* Then expect {@code expectedValue}.
*
* @param expectedValue
*/
void expect(@Nullable Object expectedValue) {

Arguments fixture;
if (empty) {
fixture = Arguments.of(new org.bson.Document(), typeUnderTest, valueMappingFunction, expectedValue,
"Empty document expecting '%s' at type %s".formatted(expectedValue, typeUnderTest.getSimpleName()));
} else {

String valueDescription = (documentValue == null ? "null"
: (documentValue + " (" + documentValue.getClass().getSimpleName()) + ")");

fixture = Arguments.of(new org.bson.Document(field, documentValue), typeUnderTest, valueMappingFunction,
expectedValue, "Field '%s' with value %s expecting '%s' at type %s".formatted(field, valueDescription,
expectedValue, typeUnderTest.getSimpleName()));
}

fixtures.add(fixture);
}
}

}

@Test // DATAMONGO-259
void writesPlainMapOfCollectionsCorrectly() {

Expand Down Expand Up @@ -2931,7 +3061,8 @@ void shouldNotSplitKeyNamesWithDotOnReadOfNestedIfFieldTypeIsKey() {

org.bson.Document source = new org.bson.Document("nested", new org.bson.Document("field.name.with.dots", "A"));

WrapperForTypeWithPropertyHavingDotsInFieldName target = converter.read(WrapperForTypeWithPropertyHavingDotsInFieldName.class, source);
WrapperForTypeWithPropertyHavingDotsInFieldName target = converter
.read(WrapperForTypeWithPropertyHavingDotsInFieldName.class, source);
assertThat(target.nested).isNotNull();
assertThat(target.nested.value).isEqualTo("A");
}
Expand Down Expand Up @@ -2964,14 +3095,16 @@ void readShouldAllowDotsInMapKeyNameIfConfigured() {
person.firstname = "bart";
person.lastname = "simpson";

org.bson.Document source = new org.bson.Document("mapOfPersons", new org.bson.Document("map.key.with.dots", write(person)));
org.bson.Document source = new org.bson.Document("mapOfPersons",
new org.bson.Document("map.key.with.dots", write(person)));

ClassWithMapProperty target = converter.read(ClassWithMapProperty.class, source);

assertThat(target.mapOfPersons).containsEntry("map.key.with.dots", person);
}

@ValueSource(classes = { ComplexIdAndNoAnnotation.class, ComplexIdAndIdAnnotation.class, ComplexIdAndMongoIdAnnotation.class, ComplexIdAndFieldAnnotation.class })
@ValueSource(classes = { ComplexIdAndNoAnnotation.class, ComplexIdAndIdAnnotation.class,
ComplexIdAndMongoIdAnnotation.class, ComplexIdAndFieldAnnotation.class })
@ParameterizedTest // GH-4524
void projectShouldReadComplexIdType(Class<?> projectionTargetType) {

Expand Down Expand Up @@ -2999,49 +3132,6 @@ org.bson.Document write(Object source) {
return target;
}

private static Stream<Arguments> listMapSetReadingSource() {

Function<CollectionWrapper, Object> contacts = CollectionWrapper::getContacts;
Function<CollectionWrapper, Object> contactsSet = CollectionWrapper::getContactsSet;
Function<CollectionWrapper, Object> autoInitList = CollectionWrapper::getAutoInitList;
Function<ClassWithMapProperty, Object> map = ClassWithMapProperty::getMap;
Function<ClassWithMapProperty, Object> autoInitMap = ClassWithMapProperty::getAutoInitMap;

return Stream.of( //

// List
Arguments.of(new org.bson.Document("contacts", Collections.emptyList()), CollectionWrapper.class, contacts,
Collections.emptyList()),
Arguments.of(new org.bson.Document("contacts", null), CollectionWrapper.class, contacts, null),
Arguments.of(new org.bson.Document(), CollectionWrapper.class, contacts, null),

// ctor initialized List
Arguments.of(new org.bson.Document("autoInitList", Collections.emptyList()), CollectionWrapper.class,
autoInitList, Collections.emptyList()),
Arguments.of(new org.bson.Document("autoInitList", null), CollectionWrapper.class, autoInitList, null),
Arguments.of(new org.bson.Document(), CollectionWrapper.class, autoInitList,
Collections.singletonList("spring")),

// Set
Arguments.of(new org.bson.Document("contactsSet", Collections.emptyList()), CollectionWrapper.class,
contactsSet, Collections.emptySet()),
Arguments.of(new org.bson.Document("contactsSet", null), CollectionWrapper.class, contactsSet, null),
Arguments.of(new org.bson.Document(), CollectionWrapper.class, contactsSet, null),

// Map
Arguments.of(new org.bson.Document("map", new org.bson.Document()), ClassWithMapProperty.class, map,
Collections.emptyMap()),
Arguments.of(new org.bson.Document("map", null), ClassWithMapProperty.class, map, null),
Arguments.of(new org.bson.Document(), ClassWithMapProperty.class, map, null),

// ctor initialized Map
Arguments.of(new org.bson.Document("autoInitMap", new org.bson.Document()), ClassWithMapProperty.class,
autoInitMap, Collections.emptyMap()),
Arguments.of(new org.bson.Document("autoInitMap", null), ClassWithMapProperty.class, autoInitMap, null),
Arguments.of(new org.bson.Document(), ClassWithMapProperty.class, autoInitMap,
Collections.singletonMap("spring", "data")));
}

static class GenericType<T> {
T content;
}
Expand Down Expand Up @@ -3135,7 +3225,9 @@ public boolean equals(Object o) {
return false;
}
Person person = (Person) o;
return Objects.equals(id, person.id) && Objects.equals(birthDate, person.birthDate) && Objects.equals(firstname, person.firstname) && Objects.equals(lastname, person.lastname) && Objects.equals(addresses, person.addresses);
return Objects.equals(id, person.id) && Objects.equals(birthDate, person.birthDate)
&& Objects.equals(firstname, person.firstname) && Objects.equals(lastname, person.lastname)
&& Objects.equals(addresses, person.addresses);
}

@Override
Expand Down Expand Up @@ -3922,10 +4014,12 @@ static class WithFieldWrite {
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Integer writeAlways;

@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.NON_NULL) Person writeNonNullPerson;

@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Person writeAlwaysPerson;

}
Expand Down Expand Up @@ -4157,8 +4251,7 @@ static class WrapperForTypeWithPropertyHavingDotsInFieldName {

static class WithPropertyHavingDotsInFieldName {

@Field(name = "field.name.with.dots", nameType = Type.KEY)
String value;
@Field(name = "field.name.with.dots", nameType = Type.KEY) String value;
}

static class ComplexIdAndFieldAnnotation {
Expand Down
11 changes: 7 additions & 4 deletions src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,15 @@ calling `get()` before the actual conversion
.Collection Handling
[NOTE]
====
Collection handing depends on the actual values retrieved from the MongoDB.
Collection handling depends on the actual values returned by MongoDB.
* If a document does **not** contain the field mapped to a collection, the mapping will not touch the property.
* If a document does **not** contain a field mapped to a collection, the mapping will not update the property.
Which means the value will remain `null`, a java default or any value set during object creation.
* If the document contains the field to be mapped, but the field holds a `null` value (like: `{ 'list' : null }`), the property value is set to `null` overriding any default value set during object creation.
* If the document contains the field to be mapped to a collection which is **not** `null` (like: `{ 'list' : [ ... ] }`), the collection is populated with the mapped values overriding any default value set during object creation.
* If a document contains a field to be mapped, but the field holds a `null` value (like: `{ 'list' : null }`), the property value is set to `null`.
* If a document contains a field to be mapped to a collection which is **not** `null` (like: `{ 'list' : [ ... ] }`), the collection is populated with the mapped values.
Generally, if you use constructor creation, then you can get hold of the value to be set.
Property population can make use of default initialization values if a property value is not being provided by a query response.
====

[[mapping-configuration]]
Expand Down

0 comments on commit 4aec4f3

Please sign in to comment.