diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index ad36913a8..0e2bd36df 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -133,6 +133,16 @@ kotlin-reflect true + + org.jetbrains.kotlinx + kotlinx-coroutines-core + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + io.mockk mockk diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java index a1a81dfce..fa19e074e 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultNeo4jClient.java @@ -148,11 +148,11 @@ class DefaultNeo4jClient implements Neo4jClient { @Override public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { - Neo4jClient.MappingSpec, Collection, T> mappingSpec = this + Neo4jClient.MappingSpec mappingSpec = this .query(preparedQuery.getCypherQuery()) .bindAll(preparedQuery.getParameters()) .fetchAs(preparedQuery.getResultType()); - Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec = preparedQuery + Neo4jClient.RecordFetchSpec fetchSpec = preparedQuery .getOptionalMappingFunction() .map(f -> mappingSpec.mappedBy(f)) .orElse(mappingSpec); @@ -249,14 +249,14 @@ class DefaultNeo4jClient implements Neo4jClient { } @Override - public MappingSpec, Collection, T> fetchAs(Class targetClass) { + public MappingSpec fetchAs(Class targetClass) { return new DefaultRecordFetchSpec(this.targetDatabase, this.runnableStatement, new SingleValueMappingFunction(conversionService, targetClass)); } @Override - public RecordFetchSpec>, Collection>, Map> fetch() { + public RecordFetchSpec> fetch() { return new DefaultRecordFetchSpec<>( this.targetDatabase, @@ -273,8 +273,7 @@ class DefaultNeo4jClient implements Neo4jClient { } } - class DefaultRecordFetchSpec - implements RecordFetchSpec, Collection, T>, MappingSpec, Collection, T> { + class DefaultRecordFetchSpec implements RecordFetchSpec, MappingSpec { private final String targetDatabase; @@ -290,7 +289,7 @@ class DefaultNeo4jClient implements Neo4jClient { } @Override - public RecordFetchSpec, Collection, T> mappedBy( + public RecordFetchSpec mappedBy( @SuppressWarnings("HiddenField") BiFunction mappingFunction) { this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); @@ -368,10 +367,9 @@ class DefaultNeo4jClient implements Neo4jClient { final class DefaultExecutableQuery implements ExecutableQuery { private final PreparedQuery preparedQuery; - private final Neo4jClient.RecordFetchSpec, Collection, T> fetchSpec; + private final Neo4jClient.RecordFetchSpec fetchSpec; - DefaultExecutableQuery(PreparedQuery preparedQuery, - RecordFetchSpec, Collection, T> fetchSpec) { + DefaultExecutableQuery(PreparedQuery preparedQuery, RecordFetchSpec fetchSpec) { this.preparedQuery = preparedQuery; this.fetchSpec = fetchSpec; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java index fc281c1d5..553645888 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/DefaultReactiveNeo4jClient.java @@ -38,9 +38,7 @@ import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.types.TypeSystem; -import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; import org.neo4j.springframework.data.core.Neo4jClient.OngoingBindSpec; -import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; import org.neo4j.springframework.data.core.convert.Neo4jConversions; import org.reactivestreams.Publisher; import org.springframework.core.convert.ConversionService; @@ -89,8 +87,8 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return Mono.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase), holder -> func.apply(holder.getRxStatementRunner()), RxStatementRunnerHolder::getCommit, - RxStatementRunnerHolder::getRollback); - + (holder, ex) -> holder.getRollback(), + RxStatementRunnerHolder::getCommit); } Flux doInStatementRunnerForFlux(final String targetDatabase, Function> func) { @@ -98,34 +96,35 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return Flux.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase), holder -> func.apply(holder.getRxStatementRunner()), RxStatementRunnerHolder::getCommit, - RxStatementRunnerHolder::getRollback); + (holder, ex) -> holder.getRollback(), + RxStatementRunnerHolder::getCommit); } @Override - public ReactiveRunnableSpec query(String cypher) { + public RunnableSpec query(String cypher) { return query(() -> cypher); } @Override - public ReactiveRunnableSpec query(Supplier cypherSupplier) { - return new DefaultReactiveRunnableSpec(cypherSupplier); + public RunnableSpec query(Supplier cypherSupplier) { + return new DefaultRunnableSpec(cypherSupplier); } @Override - public OngoingReactiveDelegation delegateTo(Function> callback) { - return new DefaultReactiveRunnableDelegation<>(callback); + public OngoingDelegation delegateTo(Function> callback) { + return new DefaultRunnableDelegation<>(callback); } @Override public ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery) { Class resultType = preparedQuery.getResultType(); - Neo4jClient.MappingSpec, Flux, T> mappingSpec = this + MappingSpec mappingSpec = this .query(preparedQuery.getCypherQuery()) .bindAll(preparedQuery.getParameters()) .fetchAs(resultType); - Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec = preparedQuery + RecordFetchSpec fetchSpec = preparedQuery .getOptionalMappingFunction() .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)) .orElse(mappingSpec); @@ -133,7 +132,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { return new DefaultReactiveExecutableQuery<>(fetchSpec); } - class DefaultReactiveRunnableSpec implements ReactiveRunnableSpec { + class DefaultRunnableSpec implements RunnableSpec { private final Supplier cypherSupplier; @@ -141,18 +140,18 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private final NamedParameters parameters = new NamedParameters(); - DefaultReactiveRunnableSpec(Supplier cypherSupplier) { + DefaultRunnableSpec(Supplier cypherSupplier) { this.cypherSupplier = cypherSupplier; } @Override - public ReactiveRunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { + public RunnableSpecTightToDatabase in(@SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = verifyDatabaseName(targetDatabase); return this; } - class DefaultOngoingBindSpec implements OngoingBindSpec { + class DefaultOngoingBindSpec implements OngoingBindSpec { @Nullable private final T value; @@ -162,14 +161,14 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } @Override - public ReactiveRunnableSpecTightToDatabase to(String name) { + public RunnableSpecTightToDatabase to(String name) { - DefaultReactiveRunnableSpec.this.parameters.add(name, value); - return DefaultReactiveRunnableSpec.this; + DefaultRunnableSpec.this.parameters.add(name, value); + return DefaultRunnableSpec.this; } @Override - public ReactiveRunnableSpecTightToDatabase with(Function> binder) { + public RunnableSpecTightToDatabase with(Function> binder) { Assert.notNull(binder, "Binder is required."); @@ -178,42 +177,41 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } @Override - public OngoingBindSpec bind(@Nullable Object value) { + public OngoingBindSpec bind(@Nullable Object value) { return new DefaultOngoingBindSpec(value); } @Override - public ReactiveRunnableSpecTightToDatabase bindAll(Map newParameters) { + public RunnableSpecTightToDatabase bindAll(Map newParameters) { this.parameters.addAll(newParameters); return this; } @Override - public MappingSpec, Flux, R> fetchAs(Class targetClass) { + public MappingSpec fetchAs(Class targetClass) { - return new DefaultReactiveRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters, + return new DefaultRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters, new SingleValueMappingFunction(conversionService, targetClass)); } @Override - public RecordFetchSpec>, Flux>, Map> fetch() { + public RecordFetchSpec> fetch() { - return new DefaultReactiveRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, + return new DefaultRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, (t, r) -> r.asMap()); } @Override public Mono run() { - return new DefaultReactiveRecordFetchSpec<>( + return new DefaultRecordFetchSpec<>( this.targetDatabase, this.cypherSupplier, this.parameters).run(); } } - class DefaultReactiveRecordFetchSpec - implements RecordFetchSpec, Flux, T>, MappingSpec, Flux, T> { + class DefaultRecordFetchSpec implements RecordFetchSpec, MappingSpec { private final String targetDatabase; @@ -223,12 +221,12 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { private BiFunction mappingFunction; - DefaultReactiveRecordFetchSpec(String targetDatabase, Supplier cypherSupplier, + DefaultRecordFetchSpec(String targetDatabase, Supplier cypherSupplier, NamedParameters parameters) { this(targetDatabase, cypherSupplier, parameters, null); } - DefaultReactiveRecordFetchSpec( + DefaultRecordFetchSpec( String targetDatabase, Supplier cypherSupplier, NamedParameters parameters, @Nullable BiFunction mappingFunction) { this.targetDatabase = targetDatabase; @@ -238,7 +236,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } @Override - public RecordFetchSpec, Flux, T> mappedBy(BiFunction mappingFunction) { + public RecordFetchSpec mappedBy(BiFunction mappingFunction) { this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction); return this; @@ -294,24 +292,24 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { } } - class DefaultReactiveRunnableDelegation implements ReactiveRunnableDelegation, OngoingReactiveDelegation { + class DefaultRunnableDelegation implements RunnableDelegation, OngoingDelegation { private final Function> callback; private String targetDatabase; - DefaultReactiveRunnableDelegation(Function> callback) { + DefaultRunnableDelegation(Function> callback) { this(callback, null); } - DefaultReactiveRunnableDelegation(Function> callback, + DefaultRunnableDelegation(Function> callback, @Nullable String targetDatabase) { this.callback = callback; this.targetDatabase = targetDatabase; } @Override - public ReactiveRunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) { + public RunnableDelegation in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) { this.targetDatabase = verifyDatabaseName(targetDatabase); return this; @@ -330,9 +328,9 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient { final class DefaultReactiveExecutableQuery implements ExecutableQuery { - private final Neo4jClient.RecordFetchSpec, Flux, T> fetchSpec; + private final RecordFetchSpec fetchSpec; - DefaultReactiveExecutableQuery(RecordFetchSpec, Flux, T> fetchSpec) { + DefaultReactiveExecutableQuery(RecordFetchSpec fetchSpec) { this.fetchSpec = fetchSpec; } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java index 25f4aa206..749bb7673 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jClient.java @@ -124,14 +124,14 @@ public interface Neo4jClient { * @param The type of the class * @return A mapping spec that allows specifying a mapping function. */ - MappingSpec, Collection, T> fetchAs(Class targetClass); + MappingSpec fetchAs(Class targetClass); /** * Fetch all records mapped into generic maps * * @return A fetch specification that maps into generic maps. */ - RecordFetchSpec>, Collection>, Map> fetch(); + RecordFetchSpec> fetch(); /** * Execute the query and discard the results. It returns the drivers result summary, including various counters @@ -186,12 +186,10 @@ public interface Neo4jClient { } /** - * @param The type of the class holding zero or one result element - * @param The type of the class holding zero or more result elements * @param The resulting type of this mapping * @since 1.0 */ - interface MappingSpec extends RecordFetchSpec { + interface MappingSpec extends RecordFetchSpec { /** * The mapping function is responsible to turn one record into one domain object. It will receive the record @@ -200,37 +198,35 @@ public interface Neo4jClient { * @param mappingFunction The mapping function used to create new domain objects * @return A specification how to fetch one or more records. */ - RecordFetchSpec mappedBy(BiFunction mappingFunction); + RecordFetchSpec mappedBy(BiFunction mappingFunction); } /** - * @param The type of the class holding zero or one result element - * @param The type of the class holding zero or more result elements * @param The type to which the fetched records are eventually mapped * @since 1.0 */ - interface RecordFetchSpec { + interface RecordFetchSpec { /** * Fetches exactly one record and throws an exception if there are more entries. * * @return The one and only record. */ - S one(); + Optional one(); /** * Fetches only the first record. Returns an empty holder if there are no records. * * @return The first record if any. */ - S first(); + Optional first(); /** * Fetches all records. * * @return All records. */ - M all(); + Collection all(); } /** diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java index d334a7e78..15b6ae0f8 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jClient.java @@ -22,17 +22,18 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; import org.neo4j.driver.reactive.RxStatementRunner; import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.types.TypeSystem; import org.neo4j.springframework.data.core.Neo4jClient.BindSpec; -import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec; -import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec; import org.springframework.core.log.LogAccessor; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -61,7 +62,7 @@ public interface ReactiveNeo4jClient { * @param cypher The cypher code that shall be executed * @return A new CypherSpec */ - ReactiveRunnableSpec query(String cypher); + RunnableSpec query(String cypher); /** * Entrypoint for creating a new Cypher query based on a supplier. Doesn't matter at this point whether it's a match, @@ -71,7 +72,7 @@ public interface ReactiveNeo4jClient { * @param cypherSupplier A supplier of arbitrary Cypher code * @return A runnable query specification. */ - ReactiveRunnableSpec query(Supplier cypherSupplier); + RunnableSpec query(Supplier cypherSupplier); /** * Delegates interaction with the default database to the given callback. @@ -80,7 +81,7 @@ public interface ReactiveNeo4jClient { * @param The type of the result being produced * @return A single publisher containing none or exactly one element that will be produced by the callback */ - OngoingReactiveDelegation delegateTo(Function> callback); + OngoingDelegation delegateTo(Function> callback); /** * Takes a prepared query, containing all the information about the cypher template to be used, needed parameters and @@ -92,11 +93,55 @@ public interface ReactiveNeo4jClient { */ ExecutableQuery toExecutableQuery(PreparedQuery preparedQuery); + /** + * @param The resulting type of this mapping + * @since 1.0 + */ + interface MappingSpec extends RecordFetchSpec { + + /** + * The mapping function is responsible to turn one record into one domain object. It will receive the record + * itself and in addition, the type system that the Neo4j Java-Driver used while executing the query. + * + * @param mappingFunction The mapping function used to create new domain objects + * @return A specification how to fetch one or more records. + */ + RecordFetchSpec mappedBy(BiFunction mappingFunction); + } + + /** + * @param The type to which the fetched records are eventually mapped + * @since 1.0 + */ + interface RecordFetchSpec { + + /** + * Fetches exactly one record and throws an exception if there are more entries. + * + * @return The one and only record. + */ + Mono one(); + + /** + * Fetches only the first record. Returns an empty holder if there are no records. + * + * @return The first record if any. + */ + Mono first(); + + /** + * Fetches all records. + * + * @return All records. + */ + Flux all(); + } + /** * Contract for a runnable query that can be either run returning it's result, run without results or be parameterized. * @since 1.0 */ - interface ReactiveRunnableSpec extends ReactiveRunnableSpecTightToDatabase { + interface RunnableSpec extends RunnableSpecTightToDatabase { /** * Pins the previously defined query to a specific database. @@ -104,14 +149,14 @@ public interface ReactiveNeo4jClient { * @param targetDatabase selected database to use * @return A runnable query specification that is now tight to a given database. */ - ReactiveRunnableSpecTightToDatabase in(String targetDatabase); + RunnableSpecTightToDatabase in(String targetDatabase); } /** * Contract for a runnable query inside a dedicated database. * @since 1.0 */ - interface ReactiveRunnableSpecTightToDatabase extends BindSpec { + interface RunnableSpecTightToDatabase extends BindSpec { /** * Create a mapping for each record return to a specific type. @@ -120,14 +165,14 @@ public interface ReactiveNeo4jClient { * @param The type of the class * @return A mapping spec that allows specifying a mapping function */ - MappingSpec, Flux, T> fetchAs(Class targetClass); + MappingSpec fetchAs(Class targetClass); /** * Fetch all records mapped into generic maps * * @return A fetch specification that maps into generic maps */ - RecordFetchSpec>, Flux>, Map> fetch(); + RecordFetchSpec> fetch(); /** * Execute the query and discard the results. It returns the drivers result summary, including various counters @@ -144,7 +189,7 @@ public interface ReactiveNeo4jClient { * @param The type of the returned value. * @since 1.0 */ - interface OngoingReactiveDelegation extends ReactiveRunnableDelegation { + interface OngoingDelegation extends RunnableDelegation { /** * Runs the delegation in the given target database. @@ -152,7 +197,7 @@ public interface ReactiveNeo4jClient { * @param targetDatabase selected database to use * @return An ongoing delegation */ - ReactiveRunnableDelegation in(String targetDatabase); + RunnableDelegation in(String targetDatabase); } /** @@ -161,7 +206,7 @@ public interface ReactiveNeo4jClient { * @param the type that gets returned by the query * @since 1.0 */ - interface ReactiveRunnableDelegation { + interface RunnableDelegation { /** * Runs the stored callback. diff --git a/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensions.kt b/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensions.kt index 40d828ebd..a1eaff6f2 100644 --- a/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensions.kt +++ b/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensions.kt @@ -42,30 +42,30 @@ fun Neo4jClient.OngoingDelegation.inDatabase(targetDatabase: Strin `in`(targetDatabase) /** - * An implementation of a fetch spec that replaces Java's Optional with a nullable. + * A fetch spec that replaces Java's Optional with a nullable. * @author Michael J. Simons */ -class DelegatingFetchSpec(private val delegate: Neo4jClient.RecordFetchSpec, Collection, T>) : Neo4jClient.RecordFetchSpec, T> { - override fun one(): T? = delegate.one().orElse(null) +class KRecordFetchSpec (private val delegate: Neo4jClient.RecordFetchSpec) { + fun one(): T? = delegate.one().orElse(null) - override fun first(): T = delegate.first().orElse(null) + fun first(): T = delegate.first().orElse(null) - override fun all(): Collection = delegate.all() + fun all(): Collection = delegate.all() } /** - * An implementation of a mapping spec that replaces Java's Optional with a nullable. + * A mapping spec that replaces Java's Optional with a nullable. * @author Michael J. Simons */ -class DelegatingMappingSpec(private val delegate: Neo4jClient.MappingSpec, Collection, T>) : Neo4jClient.MappingSpec, T> { - override fun mappedBy(mappingFunction: BiFunction): Neo4jClient.RecordFetchSpec, T> = - DelegatingFetchSpec(delegate.mappedBy(mappingFunction)) +class KMappingSpec(private val delegate: Neo4jClient.MappingSpec) { + fun mappedBy(mappingFunction: BiFunction): KRecordFetchSpec = + KRecordFetchSpec(delegate.mappedBy(mappingFunction)) - override fun one(): T? = delegate.one().orElse(null) + fun one(): T? = delegate.one().orElse(null) - override fun first(): T = delegate.first().orElse(null) + fun first(): T = delegate.first().orElse(null) - override fun all(): Collection = delegate.all() + fun all(): Collection = delegate.all() } /** @@ -73,8 +73,8 @@ class DelegatingMappingSpec(private val delegate: Neo4jClient.MappingSp * @author Michael J. Simons * @since 1.0 */ -inline fun Neo4jClient.RunnableSpecTightToDatabase.fetchAs(): Neo4jClient.MappingSpec, T> - = DelegatingMappingSpec(fetchAs(T::class.java)) +inline fun Neo4jClient.RunnableSpecTightToDatabase.fetchAs(): KMappingSpec = + KMappingSpec(fetchAs(T::class.java)) /** * Extension for [Neo4jClient.RunnableSpecTightToDatabase.mappedBy] leveraging reified type parameters and removing @@ -82,5 +82,5 @@ inline fun Neo4jClient.RunnableSpecTightToDatabase.fetchAs(): * @author Michael J. Simons * @since 1.0 */ -inline fun Neo4jClient.RunnableSpecTightToDatabase.mappedBy(noinline mappingFunction: (TypeSystem, Record) -> T): Neo4jClient.RecordFetchSpec, T> - = DelegatingFetchSpec(fetchAs(T::class.java).mappedBy(mappingFunction)) +inline fun Neo4jClient.RunnableSpecTightToDatabase.mappedBy(noinline mappingFunction: (TypeSystem, Record) -> T) + = KRecordFetchSpec(fetchAs(T::class.java).mappedBy(mappingFunction)) diff --git a/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensions.kt b/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensions.kt index 864d37e54..0ef7943c8 100644 --- a/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensions.kt +++ b/spring-data-neo4j/src/main/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensions.kt @@ -18,27 +18,31 @@ */ package org.neo4j.springframework.data.core +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle import org.neo4j.driver.Record +import org.neo4j.driver.summary.ResultSummary import org.neo4j.driver.types.TypeSystem -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono /** - * Extension for [ReactiveNeo4jClient.ReactiveRunnableSpec.in] providing an `inDatabase` alias since `in` is a reserved keyword in Kotlin. + * Extension for [ReactiveNeo4jClient.RunnableSpec.in] providing an `inDatabase` alias since `in` is a reserved keyword in Kotlin. * * @author Michael J. Simons * @since 1.0 */ -fun ReactiveNeo4jClient.ReactiveRunnableSpec.inDatabase(targetDatabase: String): ReactiveNeo4jClient.ReactiveRunnableSpecTightToDatabase = - `in`(targetDatabase) +fun ReactiveNeo4jClient.RunnableSpec.inDatabase(targetDatabase: String): ReactiveNeo4jClient.RunnableSpecTightToDatabase + = `in`(targetDatabase) /** - * Extension for [ReactiveNeo4jClient.OngoingReactiveDelegation.in] providing an `inDatabase` alias since `in` is a reserved keyword in Kotlin. + * Extension for [ReactiveNeo4jClient.OngoingDelegation.in] providing an `inDatabase` alias since `in` is a reserved keyword in Kotlin. * * @author Michael J. Simons * @since 1.0 */ -fun ReactiveNeo4jClient.OngoingReactiveDelegation.inDatabase(targetDatabase: String): ReactiveNeo4jClient.ReactiveRunnableDelegation +fun ReactiveNeo4jClient.OngoingDelegation.inDatabase(targetDatabase: String): ReactiveNeo4jClient.RunnableDelegation = `in`(targetDatabase) /** @@ -46,7 +50,7 @@ fun ReactiveNeo4jClient.OngoingReactiveDelegation.inDatabase(targe * @author Michael J. Simons * @since 1.0 */ -inline fun ReactiveNeo4jClient.ReactiveRunnableSpecTightToDatabase.fetchAs(): Neo4jClient.MappingSpec, Flux, T> +inline fun ReactiveNeo4jClient.RunnableSpecTightToDatabase.fetchAs(): ReactiveNeo4jClient.MappingSpec = fetchAs(T::class.java) /** @@ -55,5 +59,64 @@ inline fun ReactiveNeo4jClient.ReactiveRunnableSpecTightToData * @author Michael J. Simons * @since 1.0 */ -inline fun ReactiveNeo4jClient.ReactiveRunnableSpecTightToDatabase.mappedBy(noinline mappingFunction: (TypeSystem, Record) -> T): Neo4jClient.RecordFetchSpec, Flux, T> +inline fun ReactiveNeo4jClient.RunnableSpecTightToDatabase.mappedBy(noinline mappingFunction: (TypeSystem, Record) -> T): ReactiveNeo4jClient.RecordFetchSpec = fetchAs(T::class.java).mappedBy(mappingFunction) + +/** + * Non-nullable Coroutines variant of [ReactiveNeo4jClient.RunnableSpecTightToDatabase.run]. + * + * @author Michael J. Simons + * @since 1.0 + */ +suspend inline fun ReactiveNeo4jClient.RunnableSpecTightToDatabase.await(): ResultSummary = + run().awaitSingle() + +/** + * Nullable Coroutines variant of [ReactiveNeo4jClient.RecordFetchSpec.one]. + * + * @author Michael J. Simons + * @since 1.0 + */ +suspend inline fun ReactiveNeo4jClient.RecordFetchSpec.awaitOneOrNull(): T? = one().awaitFirstOrNull() + +/** + * Nullable Coroutines variant of [ReactiveNeo4jClient.RecordFetchSpec.first]. + * + * @author Michael J. Simons + * @since 1.0 + */ +suspend inline fun ReactiveNeo4jClient.RecordFetchSpec.awaitFirstOrNull(): T? = first().awaitFirstOrNull() + +/** + * Coroutines [Flow] variant of [ReactiveNeo4jClient.RecordFetchSpec.all]. + * + * @author Michael J. Simons + * @since 1.0 + */ +@ExperimentalCoroutinesApi +inline fun ReactiveNeo4jClient.RecordFetchSpec.fetchAll(): Flow = all().asFlow() + +/** + * Coroutines [Flow] variant of [ReactiveNeo4jClient.ExecutableQuery.getResults]. + * + * @author Michael J. Simons + * @since 1.0 + */ +@ExperimentalCoroutinesApi +inline fun ReactiveNeo4jClient.ExecutableQuery.fetchAllResults(): Flow = results.asFlow() + +/** + * Nullable Coroutines variant of [ReactiveNeo4jClient.ExecutableQuery.getSingleResult]. + * + * @author Michael J. Simons + * @since 1.0 + */ +suspend inline fun ReactiveNeo4jClient.ExecutableQuery.awaitSingleResultOrNull(): T? = singleResult.awaitFirstOrNull() + +/** + * Nullable Coroutines variant of [ReactiveNeo4jClient.RunnableDelegation.run]. + * + * @author Michael J. Simons + * @since 1.0 + */ +suspend inline fun ReactiveNeo4jClient.RunnableDelegation.awaitFirstOrNull(): T? = run().awaitFirstOrNull() diff --git a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensionsTest.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensionsTest.kt index 8949d4f5f..dde33bf45 100644 --- a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensionsTest.kt +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/Neo4jClientExtensionsTest.kt @@ -52,8 +52,8 @@ class Neo4jClientExtensionsTest { val runnableSpec = mockk(relaxed = true) - val mappingSpec: Neo4jClient.RecordFetchSpec, String> = - runnableSpec.mappedBy { _, record -> "Foo" }; + val mappingSpec: KRecordFetchSpec = + runnableSpec.mappedBy { _, _ -> "Foo" }; verify(exactly = 1) { runnableSpec.fetchAs(String::class.java) } } diff --git a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensionsTest.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensionsTest.kt index fbde43f8a..bcf8d119d 100644 --- a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensionsTest.kt +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/core/ReactiveNeo4jClientExtensionsTest.kt @@ -18,9 +18,15 @@ */ package org.neo4j.springframework.data.core +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.neo4j.driver.summary.ResultSummary import reactor.core.publisher.Flux import reactor.core.publisher.Mono @@ -32,7 +38,7 @@ class ReactiveNeo4jClientExtensionsTest { @Test fun `RunnableSpec#inDatabase(targetDatabase) extension should call its Java counterpart`() { - val runnableSpec = mockk(relaxed = true) + val runnableSpec = mockk(relaxed = true) runnableSpec.inDatabase("foobar"); @@ -42,7 +48,7 @@ class ReactiveNeo4jClientExtensionsTest { @Test fun `OngoingDelegation#inDatabase(targetDatabase) extension should call its Java counterpart`() { - val ongoingDelegation = mockk>(relaxed = true) + val ongoingDelegation = mockk>(relaxed = true) ongoingDelegation.inDatabase("foobar"); @@ -52,11 +58,173 @@ class ReactiveNeo4jClientExtensionsTest { @Test fun `ReactiveRunnableDelegation#fetchAs() extension should call its Java counterpart`() { - val runnableSpec = mockk(relaxed = true) + val runnableSpec = mockk(relaxed = true) - val mappingSpec : Neo4jClient.MappingSpec, Flux, String> = - runnableSpec.fetchAs(); + val mappingSpec: ReactiveNeo4jClient.MappingSpec = runnableSpec.fetchAs(); verify(exactly = 1) { runnableSpec.fetchAs(String::class.java) } } + + @Test + fun runnableSpecShouldReturnSuspendedResultSummary() { + + val runnableSpec = mockk() + val resultSummary = mockk() + every { runnableSpec.run() } returns Mono.just(resultSummary) + + runBlocking { + assertThat(runnableSpec.await()).isEqualTo(resultSummary) + } + + verify { + runnableSpec.run() + } + } + + + @Nested + inner class CoroutinesVariantsOfRunnableDelegation { + + private val runnableDelegation = mockk>() + + @Test + fun `awaitFirstOrNull should return value`() { + + every { runnableDelegation.run() } returns Mono.just("bazbar") + + runBlocking { + assertThat(runnableDelegation.awaitFirstOrNull()).isEqualTo("bazbar") + } + + verify { + runnableDelegation.run() + } + } + + @Test + fun `awaitFirstOrNull should return null`() { + + every { runnableDelegation.run() } returns Mono.empty() + + runBlocking { + assertThat(runnableDelegation.awaitFirstOrNull()).isNull() + } + + verify { + runnableDelegation.run() + } + } + } + + @Nested + inner class CoroutinesVariantsOfRecordFetchSpec { + + private val recordFetchSpec = mockk>() + + @Test + fun `awaitOne should return value`() { + every { recordFetchSpec.one() } returns Mono.just("foo") + + runBlocking { + assertThat(recordFetchSpec.awaitOneOrNull()).isEqualTo("foo") + } + verify { + recordFetchSpec.one() + } + } + + @Test + fun `awaitOne should return null`() { + every { recordFetchSpec.one() } returns Mono.empty() + + runBlocking { + assertThat(recordFetchSpec.awaitOneOrNull()).isNull() + } + verify { + recordFetchSpec.one() + } + } + + @Test + fun `awaitFirstOrNull should return value`() { + every { recordFetchSpec.first() } returns Mono.just("bar") + + runBlocking { + assertThat(recordFetchSpec.awaitFirstOrNull()).isEqualTo("bar") + } + verify { + recordFetchSpec.first() + } + } + + @Test + fun `awaitFirstOrNull should return null`() { + every { recordFetchSpec.first() } returns Mono.empty() + + runBlocking { + assertThat(recordFetchSpec.awaitFirstOrNull()).isNull() + } + verify { + recordFetchSpec.first() + } + } + + @Test + fun `fetchAll should return a flow of thing`() { + + every { recordFetchSpec.all() } returns Flux.just("foo", "bar") + + runBlocking { + assertThat(recordFetchSpec.fetchAll().toList()).contains("foo", "bar") + } + + verify { + recordFetchSpec.all() + } + } + } + + @Nested + inner class CoroutinesVariantsOfExecutableQuery { + + private val executableQuery = mockk>() + + @Test + fun `fetchAllResults should return a flow of thing`() { + + every { executableQuery.results } returns Flux.just("foo", "bar") + + runBlocking { + assertThat(executableQuery.fetchAllResults().toList()).contains("foo", "bar") + } + + verify { + executableQuery.results + } + } + + @Test + fun `awaitSingleResultOrNull should return value`() { + every { executableQuery.singleResult } returns Mono.just("baz") + + runBlocking { + assertThat(executableQuery.awaitSingleResultOrNull()).isEqualTo("baz") + } + verify { + executableQuery.singleResult + } + } + + @Test + fun `awaitFirstOrNull should return null`() { + every { executableQuery.singleResult } returns Mono.empty() + + runBlocking { + assertThat(executableQuery.awaitSingleResultOrNull()).isNull() + } + verify { + executableQuery.singleResult + } + } + } } diff --git a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/reactive/ReactiveNeo4jClientKotlinInteropIT.kt b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/reactive/ReactiveNeo4jClientKotlinInteropIT.kt index df9645281..3f9c2b9de 100644 --- a/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/reactive/ReactiveNeo4jClientKotlinInteropIT.kt +++ b/spring-data-neo4j/src/test/kotlin/org/neo4j/springframework/data/integration/reactive/ReactiveNeo4jClientKotlinInteropIT.kt @@ -18,16 +18,18 @@ */ package org.neo4j.springframework.data.integration.reactive +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.neo4j.driver.Driver +import org.neo4j.driver.Record import org.neo4j.driver.Values +import org.neo4j.driver.types.TypeSystem import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig -import org.neo4j.springframework.data.core.ReactiveNeo4jClient -import org.neo4j.springframework.data.core.fetchAs -import org.neo4j.springframework.data.core.mappedBy +import org.neo4j.springframework.data.core.* import org.neo4j.springframework.data.test.Neo4jExtension import org.neo4j.springframework.data.test.Neo4jIntegrationTest import org.springframework.beans.factory.annotation.Autowired @@ -100,6 +102,33 @@ class ReactiveNeo4jClientKotlinInteropIT @Autowired constructor( .verifyComplete(); } + @Test + fun `The reactive Neo4j client should be usable with Co-Routines`() { + + val recordToArtist: (TypeSystem, Record) -> Artist = { _, r -> Artist(r["m"]["name"].asString()) } + + runBlocking { + val artists = neo4jClient + .query("MATCH (m:Member) RETURN m ORDER BY m.name ASC") + .mappedBy(recordToArtist) + .fetchAll() + .toList() + + assertThat(artists).hasSize(7) + assertThat(artists.map { it.name }).contains("Bela", "Roger") + } + + runBlocking { + val freddie = neo4jClient + .query("MATCH (m:Member) WHERE m.name =~ \$needle RETURN m ORDER BY m.name ASC") + .bind("Fre.*").to("needle") + .mappedBy(recordToArtist) + .awaitOneOrNull() + + assertThat(freddie).isNotNull + } + } + @Configuration @EnableTransactionManagement open class Config : AbstractReactiveNeo4jConfig() {