diff --git a/driver/clirr-ignored-differences.xml b/driver/clirr-ignored-differences.xml index e72439e80b..37976805c7 100644 --- a/driver/clirr-ignored-differences.xml +++ b/driver/clirr-ignored-differences.xml @@ -633,4 +633,16 @@ org.neo4j.driver.net.ServerAddress of(java.lang.String) + + org/neo4j/driver/Value + 7012 + java.lang.Object as(java.lang.Class) + + + + org/neo4j/driver/Record + 7012 + java.lang.Object as(java.lang.Class) + + diff --git a/driver/src/main/java/module-info.java b/driver/src/main/java/module-info.java index a3038b569c..d014737e33 100644 --- a/driver/src/main/java/module-info.java +++ b/driver/src/main/java/module-info.java @@ -29,6 +29,7 @@ exports org.neo4j.driver.util; exports org.neo4j.driver.exceptions; exports org.neo4j.driver.exceptions.value; + exports org.neo4j.driver.mapping; requires org.neo4j.bolt.connection; requires org.neo4j.bolt.connection.netty; diff --git a/driver/src/main/java/org/neo4j/driver/Record.java b/driver/src/main/java/org/neo4j/driver/Record.java index 50cc719f99..cd662ecf4e 100644 --- a/driver/src/main/java/org/neo4j/driver/Record.java +++ b/driver/src/main/java/org/neo4j/driver/Record.java @@ -19,9 +19,14 @@ import java.util.List; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.NoSuchRecordException; +import org.neo4j.driver.exceptions.value.LossyCoercion; +import org.neo4j.driver.exceptions.value.Uncoercible; +import org.neo4j.driver.mapping.Property; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.MapAccessorWithDefaultValue; import org.neo4j.driver.util.Immutable; import org.neo4j.driver.util.Pair; +import org.neo4j.driver.util.Preview; /** * Container for Cypher result values. @@ -78,4 +83,75 @@ public interface Record extends MapAccessorWithDefaultValue { * @throws NoSuchRecordException if the associated underlying record is not available */ List> fields(); + + /** + * Maps values of this record to properties of the given type providing that is it supported. + *

+ * Example (using the Neo4j Movies Database): + *

+     * {@code
+     * // assuming the following Java record
+     * public record MovieInfo(String title, String director, List actors) {}
+     * // the record values may be mapped to MovieInfo instances
+     * var movies = driver.executableQuery("MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person) RETURN movie.title as title, director.name AS director, collect(actor.name) AS actors")
+     *         .execute()
+     *         .records()
+     *         .stream()
+     *         .map(record -> record.as(MovieInfo.class))
+     *         .toList();
+     * }
+     * 
+ *

+ * Note that Object Mapping is an alternative to accessing the user-defined values in a {@link MapAccessor}. + * If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like + * Spring Data Neo4j. + *

+ * The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the + * constructor parameters must either have {@link Property} annotation or have a matching name that is available + * at runtime (note that the constructor parameter names are typically changed by the compiler unless either the + * compiler {@code -parameters} option is used or they belong to the cannonical constructor of + * {@link java.lang.Record java.lang.Record}). The name matching is case-sensitive. + *

+ * Additionally, the {@link Property} annotation may be used when mapping a property with a different name to + * {@link java.lang.Record java.lang.Record} cannonical constructor parameter. + *

+ * The constructor selection criteria is the following (top priority first): + *

    + *
  1. Maximum matching properties.
  2. + *
  3. Minimum mismatching properties.
  4. + *
+ * The constructor search is done in the order defined by the {@link Class#getDeclaredConstructors} and is + * finished either when a full match is found with no mismatches or once all constructors have been visited. + *

+ * At least 1 property match must be present for mapping to work. + *

+ * A {@code null} value is used for arguments that don't have a matching property. If the argument does not accept + * {@code null} value (this includes primitive types), an alternative constructor that excludes it must be + * available. + *

+ * Types with generic parameters defined at the class level are not supported. However, constructor arguments with + * specific types are permitted, see the {@code actors} parameter in the example above. + *

+ * On the contrary, the following record would not be mapped because the type information is insufficient: + *

+     * {@code
+     * public record MovieInfo(String title, String director, List actors) {}
+     * }
+     * 
+ * Wildcard type value is not supported. + * + * @param targetClass the target class to map to + * @param the target type to map to + * @return the mapped value + * @throws Uncoercible when mapping to the target type is not possible + * @throws LossyCoercion when mapping cannot be achieved without losing precision + * @see Property + * @see Value#as(Class) + * @since 5.28.5 + */ + @Preview(name = "Object mapping") + default T as(Class targetClass) { + // for backwards compatibility only + throw new UnsupportedOperationException("not supported"); + } } diff --git a/driver/src/main/java/org/neo4j/driver/Value.java b/driver/src/main/java/org/neo4j/driver/Value.java index 88577f1f3d..6a7d872e51 100644 --- a/driver/src/main/java/org/neo4j/driver/Value.java +++ b/driver/src/main/java/org/neo4j/driver/Value.java @@ -30,6 +30,7 @@ import org.neo4j.driver.exceptions.value.LossyCoercion; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.value.NullValue; +import org.neo4j.driver.mapping.Property; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.IsoDuration; import org.neo4j.driver.types.MapAccessor; @@ -41,6 +42,7 @@ import org.neo4j.driver.types.Type; import org.neo4j.driver.types.TypeSystem; import org.neo4j.driver.util.Immutable; +import org.neo4j.driver.util.Preview; /** * A unit of data that adheres to the Neo4j type system. @@ -52,7 +54,7 @@ * For example, a common String value should be tested for using isString * and extracted using stringValue. *

Navigating a tree structure

- * + *

* Because Neo4j often handles dynamic structures, this interface is designed to help * you handle such structures in Java. Specifically, {@link Value} lets you navigate arbitrary tree * structures without having to resort to type casting. @@ -69,14 +71,14 @@ * } * } * - * + *

* You can retrieve the name of the second user, John, like so: *

  * {@code
  * String username = value.get("users").get(1).get("name").asString();
  * }
  * 
- * + *

* You can also easily iterate over the users: *

  * {@code
@@ -87,6 +89,7 @@
  * }
  * }
  * 
+ * * @since 1.0 */ @Immutable @@ -133,14 +136,16 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { */ Value get(int index); - /** @return The type of this value as defined in the Neo4j type system */ + /** + * @return The type of this value as defined in the Neo4j type system + */ Type type(); /** * Test if this value is a value of the given type * * @param type the given type - * @return type.isTypeOf( this ) + * @return type.isTypeOf(this) */ boolean hasType(Type type); @@ -184,7 +189,7 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { *
  • {@link TypeSystem#RELATIONSHIP()} - {@link Relationship}
  • *
  • {@link TypeSystem#PATH()} - {@link Path}
  • * - * + *

    * Note that the types in {@link TypeSystem} refers to the Neo4j type system * where {@link TypeSystem#INTEGER()} and {@link TypeSystem#FLOAT()} are both * 64-bit precision. This is why these types return java {@link Long} and @@ -197,9 +202,10 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { /** * Apply the mapping function on the value if the value is not a {@link NullValue}, or the default value if the value is a {@link NullValue}. - * @param mapper The mapping function defines how to map a {@link Value} to T. + * + * @param mapper The mapping function defines how to map a {@link Value} to T. * @param defaultValue the value to return if the value is a {@link NullValue} - * @param The return type + * @param The return type * @return The value after applying the given mapping function or the default value if the value is {@link NullValue}. */ T computeOrDefault(Function mapper, T defaultValue); @@ -218,21 +224,21 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { boolean asBoolean(boolean defaultValue); /** - * @return the value as a Java byte array, if possible. - * @throws Uncoercible if value types are incompatible. + * @return the value as a Java byte array, if possible. + * @throws Uncoercible if value types are incompatible. */ byte[] asByteArray(); /** - * @param defaultValue default to this value if the original value is a {@link NullValue} - * @return the value as a Java byte array, if possible. - * @throws Uncoercible if value types are incompatible. + * @param defaultValue default to this value if the original value is a {@link NullValue} + * @return the value as a Java byte array, if possible. + * @throws Uncoercible if value types are incompatible. */ byte[] asByteArray(byte[] defaultValue); /** - * @return the value as a Java String, if possible. - * @throws Uncoercible if value types are incompatible. + * @return the value as a Java String, if possible. + * @throws Uncoercible if value types are incompatible. */ String asString(); @@ -254,16 +260,17 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * * @return the value as a Java long. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ long asLong(); /** * Returns a Java long if no precision is lost in the conversion. + * * @param defaultValue return this default value if the value is a {@link NullValue}. * @return the value as a Java long. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ long asLong(long defaultValue); @@ -272,16 +279,17 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * * @return the value as a Java int. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ int asInt(); /** * Returns a Java int if no precision is lost in the conversion. + * * @param defaultValue return this default value if the value is a {@link NullValue}. * @return the value as a Java int. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ int asInt(int defaultValue); @@ -290,16 +298,17 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * * @return the value as a Java double. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ double asDouble(); /** * Returns a Java double if no precision is lost in the conversion. + * * @param defaultValue default to this value if the value is a {@link NullValue}. * @return the value as a Java double. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ double asDouble(double defaultValue); @@ -308,16 +317,17 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * * @return the value as a Java float. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ float asFloat(); /** * Returns a Java float if no precision is lost in the conversion. + * * @param defaultValue default to this value if the value is a {@link NullValue} * @return the value as a Java float. * @throws LossyCoercion if it is not possible to convert the value without loosing precision. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. */ float asFloat(float defaultValue); @@ -325,8 +335,8 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * If the underlying type can be viewed as a list, returns a java list of * values, where each value has been converted using {@link #asObject()}. * - * @see #asObject() * @return the value as a Java list of values, if possible + * @see #asObject() */ List asList(); @@ -334,28 +344,28 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * If the underlying type can be viewed as a list, returns a java list of * values, where each value has been converted using {@link #asObject()}. * - * @see #asObject() * @param defaultValue default to this value if the value is a {@link NullValue} * @return the value as a Java list of values, if possible + * @see #asObject() */ List asList(List defaultValue); /** * @param mapFunction a function to map from Value to T. See {@link Values} for some predefined functions, such - * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. - * @param the type of target list elements - * @see Values for a long list of built-in conversion functions + * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. + * @param the type of target list elements * @return the value as a list of T obtained by mapping from the list elements, if possible + * @see Values for a long list of built-in conversion functions */ List asList(Function mapFunction); /** - * @param mapFunction a function to map from Value to T. See {@link Values} for some predefined functions, such - * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. - * @param the type of target list elements + * @param mapFunction a function to map from Value to T. See {@link Values} for some predefined functions, such + * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. + * @param the type of target list elements * @param defaultValue default to this value if the value is a {@link NullValue} - * @see Values for a long list of built-in conversion functions * @return the value as a list of T obtained by mapping from the list elements, if possible + * @see Values for a long list of built-in conversion functions */ List asList(Function mapFunction, List defaultValue); @@ -409,14 +419,14 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { /** * @return the value as a {@link java.time.OffsetDateTime}, if possible. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. * @throws DateTimeException if zone information supplied by server is not supported by driver runtime. */ OffsetDateTime asOffsetDateTime(); /** * @return the value as a {@link ZonedDateTime}, if possible. - * @throws Uncoercible if value types are incompatible. + * @throws Uncoercible if value types are incompatible. * @throws DateTimeException if zone information supplied by server is not supported by driver runtime. */ ZonedDateTime asZonedDateTime(); @@ -501,15 +511,217 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { Map asMap(Map defaultValue); /** - * @param mapFunction a function to map from Value to T. See {@link Values} for some predefined functions, such - * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. - * @param the type of map values + * @param mapFunction a function to map from Value to T. See {@link Values} for some predefined functions, such + * as {@link Values#ofBoolean()}, {@link Values#ofList(Function)}. + * @param the type of map values * @param defaultValue default to this value if the value is a {@link NullValue} - * @see Values for a long list of built-in conversion functions * @return the value as a map from string keys to values of type T obtained from mapping he original map values, if possible + * @see Values for a long list of built-in conversion functions */ Map asMap(Function mapFunction, Map defaultValue); + /** + * Maps this value to the given type providing that is it supported. + *

    + * Basic Mapping + *

    + * Supported destination types depend on the value {@link Type}, please see the table below for more details. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Supported Mappings
    Value TypeSupported Target Types
    {@link TypeSystem#BOOLEAN}{@code boolean}, {@link Boolean}
    {@link TypeSystem#BYTES}{@code byte[]}
    {@link TypeSystem#STRING}{@link String}
    {@link TypeSystem#INTEGER}{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}
    {@link TypeSystem#FLOAT}{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}
    {@link TypeSystem#PATH}{@link Path}
    {@link TypeSystem#POINT}{@link Point}
    {@link TypeSystem#DATE}{@link LocalDate}
    {@link TypeSystem#TIME}{@link OffsetTime}
    {@link TypeSystem#LOCAL_TIME}{@link LocalTime}
    {@link TypeSystem#LOCAL_DATE_TIME}{@link LocalDateTime}
    {@link TypeSystem#DATE_TIME}{@link ZonedDateTime}, {@link OffsetDateTime}
    {@link TypeSystem#DURATION}{@link IsoDuration}, {@link java.time.Period} (only when {@code seconds = 0} and {@code nanoseconds = 0} and no overflow happens), + * {@link java.time.Duration} (only when {@code months = 0} and {@code days = 0} and no overflow happens)
    {@link TypeSystem#NULL}{@code null}
    {@link TypeSystem#LIST}{@link List}
    {@link TypeSystem#MAP}{@link Map}
    {@link TypeSystem#NODE}{@link Node}
    {@link TypeSystem#RELATIONSHIP}{@link Relationship}
    + * + *

    + * Object Mapping + *

    + * Mapping of user-defined properties to user-defined types is supported for the following value types: + *

      + *
    • {@link TypeSystem#NODE}
    • + *
    • {@link TypeSystem#RELATIONSHIP}
    • + *
    • {@link TypeSystem#MAP}
    • + *
    + *

    + * Example (using the Neo4j Movies Database): + *

    +     * {@code
    +     * // assuming the following Java record
    +     * public record Movie(String title, String tagline, long released) {}
    +     * // the nodes may be mapped to Movie instances
    +     * var movies = driver.executableQuery("MATCH (movie:Movie) RETURN movie")
    +     *         .execute()
    +     *         .records()
    +     *         .stream()
    +     *         .map(record -> record.get("movie").as(Movie.class))
    +     *         .toList();
    +     * }
    +     * 
    + *

    + * Note that Object Mapping is an alternative to accessing the user-defined values in a {@link MapAccessor}. + * If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like + * Spring Data Neo4j. + *

    + * The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the + * constructor parameters must either have {@link Property} annotation or have a matching name that is available + * at runtime (note that the constructor parameter names are typically changed by the compiler unless either the + * compiler {@code -parameters} option is used or they belong to the cannonical constructor of + * {@link java.lang.Record java.lang.Record}). The name matching is case-sensitive. + *

    + * Additionally, the {@link Property} annotation may be used when mapping a property with a different name to + * {@link java.lang.Record java.lang.Record} cannonical constructor parameter. + *

    + * The constructor selection criteria is the following (top priority first): + *

      + *
    1. Maximum matching properties.
    2. + *
    3. Minimum mismatching properties.
    4. + *
    + * The constructor search is done in the order defined by the {@link Class#getDeclaredConstructors} and is + * finished either when a full match is found with no mismatches or once all constructors have been visited. + *

    + * At least 1 property match must be present for mapping to work. + *

    + * A {@code null} value is used for arguments that don't have a matching property. If the argument does not accept + * {@code null} value (this includes primitive types), an alternative constructor that excludes it must be + * available. + *

    + * Example with optional property (using the Neo4j Movies Database): + *

    +     * {@code
    +     * // assuming the following Java record
    +     * public record Person(String name, long born) {
    +     *     // alternative constructor for values that don't have 'born' property available
    +     *     public Person(@Property("name") String name) {
    +     *         this(name, -1);
    +     *     }
    +     * }
    +     * // the nodes may be mapped to Person instances
    +     * var persons = driver.executableQuery("MATCH (person:Person) RETURN person")
    +     *         .execute()
    +     *         .records()
    +     *         .stream()
    +     *         .map(record -> record.get("person").as(Person.class))
    +     *         .toList();
    +     * }
    +     * 
    + *

    + * Types with generic parameters defined at the class level are not supported. However, constructor arguments with + * specific types are permitted. + *

    + * Example (using the Neo4j Movies Database): + *

    +     * {@code
    +     * // assuming the following Java record
    +     * public record Acted(List roles) {}
    +     * // the relationships may be mapped to Acted instances
    +     * var actedList = driver.executableQuery("MATCH ()-[acted:ACTED_IN]-() RETURN acted")
    +     *         .execute()
    +     *         .records()
    +     *         .stream()
    +     *         .map(record -> record.get("acted").as(Acted.class))
    +     *         .toList();
    +     * }
    +     * 
    + * On the contrary, the following record would not be mapped because the type information is insufficient: + *
    +     * {@code
    +     * public record Acted(List roles) {}
    +     * }
    +     * 
    + * Wildcard type value is not supported. + * + * @param targetClass the target class to map to + * @param the target type to map to + * @return the mapped value + * @throws Uncoercible when mapping to the target type is not possible + * @throws LossyCoercion when mapping cannot be achieved without losing precision + * @see Property + * @since 5.28.5 + */ + @Preview(name = "Object mapping") + default T as(Class targetClass) { + // for backwards compatibility only + throw new UnsupportedOperationException("not supported"); + } + @Override boolean equals(Object other); diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/value/ValueException.java b/driver/src/main/java/org/neo4j/driver/exceptions/value/ValueException.java index edbd0a6bd3..244dcddd42 100644 --- a/driver/src/main/java/org/neo4j/driver/exceptions/value/ValueException.java +++ b/driver/src/main/java/org/neo4j/driver/exceptions/value/ValueException.java @@ -19,6 +19,7 @@ import java.io.Serial; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.internal.GqlStatusError; +import org.neo4j.driver.util.Preview; /** * A ValueException indicates that the client has carried out an operation on values incorrectly. @@ -33,12 +34,23 @@ public class ValueException extends ClientException { * @param message the message */ public ValueException(String message) { + this(message, null); + } + + /** + * Creates a new instance. + * @param message the message + * @param cause the cause + * @since 5.28.5 + */ + @Preview(name = "Object mapping") + public ValueException(String message, Throwable cause) { super( GqlStatusError.UNKNOWN.getStatus(), GqlStatusError.UNKNOWN.getStatusDescription(message), "N/A", message, GqlStatusError.DIAGNOSTIC_RECORD, - null); + cause); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalRecord.java b/driver/src/main/java/org/neo4j/driver/internal/InternalRecord.java index 906eae8a30..954c09fb06 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalRecord.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalRecord.java @@ -30,9 +30,11 @@ import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalMapAccessorWithDefaultValue; import org.neo4j.driver.internal.util.Extract; import org.neo4j.driver.internal.util.QueryKeys; +import org.neo4j.driver.internal.value.mapping.MapAccessorMapperProvider; import org.neo4j.driver.util.Pair; public class InternalRecord extends InternalMapAccessorWithDefaultValue implements Record { @@ -116,6 +118,16 @@ public Map asMap(Function mapper) { return Extract.map(this, mapper); } + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Record.class)) { + return targetClass.cast(this); + } + return MapAccessorMapperProvider.mapper(this, targetClass) + .map(mapper -> mapper.map(this, targetClass)) + .orElseThrow(() -> new Uncoercible(getClass().getCanonicalName(), targetClass.getCanonicalName())); + } + @Override public String toString() { return format("Record<%s>", formatPairs(asMap(ofValue()))); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/BooleanValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/BooleanValue.java index 96e4ee0eb4..2671c66df5 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/BooleanValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/BooleanValue.java @@ -16,6 +16,7 @@ */ package org.neo4j.driver.internal.value; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -57,6 +58,17 @@ public boolean asBoolean() { return true; } + @SuppressWarnings("unchecked") + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Boolean.class)) { + return targetClass.cast(Boolean.TRUE); + } else if (targetClass.isAssignableFrom(boolean.class)) { + return (T) Boolean.TRUE; + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public boolean isTrue() { return true; @@ -90,6 +102,17 @@ public boolean asBoolean() { return false; } + @SuppressWarnings("unchecked") + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Boolean.class)) { + return targetClass.cast(Boolean.FALSE); + } else if (targetClass.isAssignableFrom(boolean.class)) { + return (T) Boolean.FALSE; + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public boolean isFalse() { return true; diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/BytesValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/BytesValue.java index 7eff49a6d6..c8ad70a580 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/BytesValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/BytesValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.util.Arrays; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -50,6 +51,14 @@ public byte[] asByteArray() { return val; } + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(byte[].class)) { + return targetClass.cast(asByteArray()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public Type type() { return InternalTypeSystem.TYPE_SYSTEM.BYTES(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/DateTimeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/DateTimeValue.java index d34c46201b..e2c0ca0164 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/DateTimeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/DateTimeValue.java @@ -18,6 +18,7 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -45,4 +46,14 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.DATE_TIME); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(ZonedDateTime.class)) { + return targetClass.cast(asZonedDateTime()); + } else if (targetClass.isAssignableFrom(OffsetDateTime.class)) { + return targetClass.cast(asOffsetDateTime()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/DateValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/DateValue.java index 8684ea7bff..07ab3415e9 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/DateValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/DateValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.time.LocalDate; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -39,4 +40,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.DATE); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(LocalDate.class)) { + return targetClass.cast(asLocalDate()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/DurationValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/DurationValue.java index f20fa193b0..cf37b54d13 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/DurationValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/DurationValue.java @@ -39,4 +39,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.DURATION); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(IsoDuration.class)) { + return targetClass.cast(asIsoDuration()); + } + return asMapped(targetClass); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/FloatValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/FloatValue.java index c8856b9d79..df6771ed5e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/FloatValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/FloatValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import org.neo4j.driver.exceptions.value.LossyCoercion; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -72,6 +73,29 @@ public float asFloat() { return floatVal; } + @SuppressWarnings("unchecked") + @Override + public T as(Class targetClass) { + if (targetClass.equals(double.class)) { + return (T) Double.valueOf(asDouble()); + } else if (targetClass.isAssignableFrom(Double.class)) { + return targetClass.cast(asDouble()); + } else if (targetClass.equals(long.class)) { + return (T) Long.valueOf(asLong()); + } else if (targetClass.equals(Long.class)) { + return targetClass.cast(asLong()); + } else if (targetClass.equals(int.class)) { + return (T) Integer.valueOf(asInt()); + } else if (targetClass.equals(Integer.class)) { + return targetClass.cast(asInt()); + } else if (targetClass.equals(float.class)) { + return (T) Float.valueOf(asFloat()); + } else if (targetClass.equals(Float.class)) { + return targetClass.cast(asFloat()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java index 6631b2c096..0211135a09 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import org.neo4j.driver.exceptions.value.LossyCoercion; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -65,6 +66,29 @@ public float asFloat() { return (float) val; } + @SuppressWarnings("unchecked") + @Override + public T as(Class targetClass) { + if (targetClass.equals(long.class)) { + return (T) Long.valueOf(asLong()); + } else if (targetClass.isAssignableFrom(Long.class)) { + return targetClass.cast(asLong()); + } else if (targetClass.equals(int.class)) { + return (T) Integer.valueOf(asInt()); + } else if (targetClass.equals(Integer.class)) { + return targetClass.cast(asInt()); + } else if (targetClass.equals(double.class)) { + return (T) Double.valueOf(asDouble()); + } else if (targetClass.equals(Double.class)) { + return targetClass.cast(asDouble()); + } else if (targetClass.equals(float.class)) { + return (T) Float.valueOf(asFloat()); + } else if (targetClass.equals(Float.class)) { + return targetClass.cast(asFloat()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public String toString() { return Long.toString(val); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/InternalValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/InternalValue.java index b487f45257..f41927dc24 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/InternalValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/InternalValue.java @@ -16,7 +16,10 @@ */ package org.neo4j.driver.internal.value; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import org.neo4j.driver.Value; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.AsValue; import org.neo4j.driver.internal.types.TypeConstructor; @@ -24,4 +27,18 @@ public interface InternalValue extends Value, AsValue { TypeConstructor typeConstructor(); BoltValue asBoltValue(); + + default Object as(Type type) { + if (type instanceof ParameterizedType parameterizedType) { + return as(parameterizedType); + } else if (type instanceof Class classType) { + return as(classType); + } else { + throw new Uncoercible(type().name(), type.toString()); + } + } + + default Object as(ParameterizedType type) { + throw new Uncoercible(type().name(), type.toString()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java index e0ce7dce0b..acabe97626 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java @@ -18,12 +18,14 @@ import static org.neo4j.driver.Values.ofObject; +import java.lang.reflect.ParameterizedType; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.function.Function; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.util.Extract; import org.neo4j.driver.types.Type; @@ -58,6 +60,29 @@ public List asList(Function mapFunction) { return Extract.list(values, mapFunction); } + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(List.class)) { + return targetClass.cast(asList()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + + @Override + public Object as(ParameterizedType type) { + var rawType = type.getRawType(); + if (rawType instanceof Class cls) { + if (cls.isAssignableFrom(List.class)) { + return asList(v -> { + var value = (InternalValue) v; + var typeArgument = type.getActualTypeArguments()[0]; + return value.as(typeArgument); + }); + } + } + throw new Uncoercible(type().name(), type.toString()); + } + @Override public int size() { return values.length; diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/LocalDateTimeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/LocalDateTimeValue.java index 24848279a9..90d670a316 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/LocalDateTimeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/LocalDateTimeValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.time.LocalDateTime; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -39,4 +40,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.LOCAL_DATE_TIME); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(LocalDateTime.class)) { + return targetClass.cast(asLocalDateTime()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/LocalTimeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/LocalTimeValue.java index c389410168..1b275a24c8 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/LocalTimeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/LocalTimeValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.time.LocalTime; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -39,4 +40,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.LOCAL_TIME); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(LocalTime.class)) { + return targetClass.cast(asLocalTime()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/MapValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/MapValue.java index d1d1cab1a2..c58e77c257 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/MapValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/MapValue.java @@ -20,10 +20,12 @@ import static org.neo4j.driver.Values.ofValue; import static org.neo4j.driver.internal.util.Format.formatPairs; +import java.lang.reflect.ParameterizedType; import java.util.Map; import java.util.function.Function; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.util.Extract; import org.neo4j.driver.types.Type; @@ -48,6 +50,34 @@ public Map asObject() { return asMap(ofObject()); } + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Map.class)) { + return targetClass.cast(asMap()); + } + return asMapped(targetClass); + } + + @Override + public Object as(ParameterizedType type) { + var rawType = type.getRawType(); + if (rawType instanceof Class cls) { + if (cls.isAssignableFrom(Map.class)) { + var keyType = type.getActualTypeArguments()[0]; + if (keyType instanceof Class keyCls) { + if (keyCls.isAssignableFrom(String.class)) { + var valueType = type.getActualTypeArguments()[1]; + return asMap(v -> { + var value = (InternalValue) v; + return value.as(valueType); + }); + } + } + } + } + throw new Uncoercible(type().name(), type.toString()); + } + @Override public Map asMap() { return Extract.map(val, ofObject()); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/NodeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/NodeValue.java index 1615378df6..d859db9ddd 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/NodeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/NodeValue.java @@ -39,4 +39,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.NODE); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Node.class)) { + return targetClass.cast(asNode()); + } + return asMapped(targetClass); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/NullValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/NullValue.java index 4ec87a39fd..9ad122f4a0 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/NullValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/NullValue.java @@ -16,6 +16,7 @@ */ package org.neo4j.driver.internal.value; +import java.lang.reflect.ParameterizedType; import org.neo4j.driver.Value; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -40,6 +41,16 @@ public String asString() { return "null"; } + @Override + public T as(Class targetClass) { + return null; + } + + @Override + public Object as(ParameterizedType type) { + return null; + } + @Override public Type type() { return InternalTypeSystem.TYPE_SYSTEM.NULL(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/PathValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/PathValue.java index f7f4037b86..36d0f84996 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/PathValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/PathValue.java @@ -44,4 +44,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.PATH); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Path.class)) { + return targetClass.cast(asPath()); + } + return asMapped(targetClass); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/PointValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/PointValue.java index 172376a6f3..feb8ac3e69 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/PointValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/PointValue.java @@ -16,6 +16,7 @@ */ package org.neo4j.driver.internal.value; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Point; import org.neo4j.driver.types.Type; @@ -39,4 +40,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.POINT); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Point.class)) { + return targetClass.cast(asPoint()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/RelationshipValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/RelationshipValue.java index 19683e178c..84dead0f11 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/RelationshipValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/RelationshipValue.java @@ -39,4 +39,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.RELATIONSHIP); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(Relationship.class)) { + return targetClass.cast(asRelationship()); + } + return asMapped(targetClass); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java index 7d38eeda4f..75c92f03f7 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.util.Objects; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -50,6 +51,14 @@ public String asString() { return val; } + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(String.class)) { + return targetClass.cast(asString()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public String toString() { return String.format("\"%s\"", val.replace("\"", "\\\"")); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/TimeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/TimeValue.java index c74529f444..0cbe2cb944 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/TimeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/TimeValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.time.OffsetTime; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -39,4 +40,12 @@ public Type type() { public BoltValue asBoltValue() { return new BoltValue(this, org.neo4j.bolt.connection.values.Type.TIME); } + + @Override + public T as(Class targetClass) { + if (targetClass.isAssignableFrom(OffsetTime.class)) { + return targetClass.cast(asOffsetTime()); + } + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/UnsupportedDateTimeValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/UnsupportedDateTimeValue.java index 27e31404c8..d04068b325 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/UnsupportedDateTimeValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/UnsupportedDateTimeValue.java @@ -20,6 +20,7 @@ import java.time.DateTimeException; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -40,6 +41,11 @@ public ZonedDateTime asZonedDateTime() { throw instantiateDateTimeException(); } + @Override + public T as(Class targetClass) { + throw new Uncoercible(type().name(), targetClass.getCanonicalName()); + } + @Override public Object asObject() { throw instantiateDateTimeException(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/ValueAdapter.java b/driver/src/main/java/org/neo4j/driver/internal/value/ValueAdapter.java index 3c9a0e3ca6..24d875518d 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/ValueAdapter.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/ValueAdapter.java @@ -36,6 +36,7 @@ import org.neo4j.driver.internal.types.InternalMapAccessorWithDefaultValue; import org.neo4j.driver.internal.types.TypeConstructor; import org.neo4j.driver.internal.types.TypeRepresentation; +import org.neo4j.driver.internal.value.mapping.MapAccessorMapperProvider; import org.neo4j.driver.types.Entity; import org.neo4j.driver.types.IsoDuration; import org.neo4j.driver.types.Node; @@ -343,6 +344,12 @@ public final TypeConstructor typeConstructor() { return ((TypeRepresentation) type()).constructor(); } + protected T asMapped(Class targetClass) { + return MapAccessorMapperProvider.mapper(this, targetClass) + .map(mapper -> mapper.map(this, targetClass)) + .orElseThrow(() -> new Uncoercible(type().name(), targetClass.getCanonicalName())); + } + // Force implementation @Override public abstract boolean equals(Object obj); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/Argument.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/Argument.java new file mode 100644 index 0000000000..683ab875af --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/Argument.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.lang.reflect.Type; +import org.neo4j.driver.internal.value.InternalValue; + +record Argument(String propertyName, Type type, InternalValue value) {} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ConstructorFinder.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ConstructorFinder.java new file mode 100644 index 0000000000..53b6e60204 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ConstructorFinder.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.value.InternalValue; +import org.neo4j.driver.mapping.Property; +import org.neo4j.driver.types.MapAccessor; + +class ConstructorFinder { + @SuppressWarnings("unchecked") + public Optional> findConstructor(MapAccessor mapAccessor, Class targetClass) { + PropertiesMatch bestPropertiesMatch = null; + var constructors = targetClass.getDeclaredConstructors(); + var propertyNamesSize = mapAccessor.size(); + for (var constructor : constructors) { + var accessible = false; + try { + accessible = constructor.canAccess(null); + } catch (Throwable e) { + // ignored + } + if (!accessible) { + continue; + } + var matchNumbers = matchPropertyNames(mapAccessor, constructor); + if (bestPropertiesMatch == null + || (matchNumbers.match() >= bestPropertiesMatch.match() + && matchNumbers.mismatch() < bestPropertiesMatch.mismatch())) { + bestPropertiesMatch = (PropertiesMatch) matchNumbers; + if (bestPropertiesMatch.match() == propertyNamesSize && bestPropertiesMatch.mismatch() == 0) { + break; + } + } + } + if (bestPropertiesMatch == null || bestPropertiesMatch.match() == 0) { + return Optional.empty(); + } + + return Optional.of(new ObjectMetadata<>(bestPropertiesMatch.constructor(), bestPropertiesMatch.arguments())); + } + + private PropertiesMatch matchPropertyNames(MapAccessor mapAccessor, Constructor constructor) { + var match = 0; + var mismatch = 0; + var parameters = constructor.getParameters(); + var arguments = new ArrayList(parameters.length); + for (var parameter : parameters) { + var propertyNameAnnotation = parameter.getAnnotation(Property.class); + var propertyName = propertyNameAnnotation != null ? propertyNameAnnotation.value() : parameter.getName(); + var value = mapAccessor.get(propertyName); + if (value != null) { + match++; + } else { + mismatch++; + } + arguments.add(new Argument( + propertyName, + parameter.getParameterizedType(), + value != null ? (InternalValue) value : (InternalValue) Values.NULL)); + } + return new PropertiesMatch<>(match, mismatch, constructor, arguments); + } + + private record PropertiesMatch(int match, int mismatch, Constructor constructor, List arguments) {} +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/DurationMapper.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/DurationMapper.java new file mode 100644 index 0000000000..d8a31a2faf --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/DurationMapper.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.time.Duration; +import java.time.Period; +import java.time.temporal.TemporalAmount; +import org.neo4j.driver.Value; +import org.neo4j.driver.exceptions.value.LossyCoercion; +import org.neo4j.driver.exceptions.value.Uncoercible; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.TypeSystem; + +class DurationMapper implements MapAccessorMapper { + private static final TypeSystem TS = TypeSystem.getDefault(); + + @Override + public boolean supports(MapAccessor mapAccessor, Class targetClass) { + return (mapAccessor instanceof Value value && TS.DURATION().equals(value.type())) + && (targetClass.isAssignableFrom(Period.class) || targetClass.isAssignableFrom(Duration.class)); + } + + @Override + public TemporalAmount map(MapAccessor mapAccessor, Class targetClass) { + if (mapAccessor instanceof Value value) { + var isoDuration = value.asIsoDuration(); + if (targetClass.isAssignableFrom(TemporalAmount.class)) { + return isoDuration; + } else if (targetClass.isAssignableFrom(Period.class)) { + if (isoDuration.seconds() != 0 || isoDuration.nanoseconds() != 0) { + throw new LossyCoercion(value.type().name(), targetClass.getCanonicalName()); + } + var totalMonths = isoDuration.months(); + var splitYears = totalMonths / 12; + var splitMonths = totalMonths % 12; + try { + return Period.of( + Math.toIntExact(splitYears), + Math.toIntExact(splitMonths), + Math.toIntExact(isoDuration.days())) + .normalized(); + } catch (ArithmeticException e) { + throw new LossyCoercion(value.type().name(), targetClass.getCanonicalName()); + } + } else if (targetClass.isAssignableFrom(Duration.class)) { + if (isoDuration.months() != 0 || isoDuration.days() != 0) { + throw new LossyCoercion(value.type().name(), targetClass.getCanonicalName()); + } + return Duration.ofSeconds(isoDuration.seconds()).plusNanos(isoDuration.nanoseconds()); + } + throw new Uncoercible(value.type().name(), targetClass.getCanonicalName()); + } else { + throw new Uncoercible(mapAccessor.getClass().getCanonicalName(), targetClass.getCanonicalName()); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapper.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapper.java new file mode 100644 index 0000000000..cfc41b3bf0 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import org.neo4j.driver.types.MapAccessor; + +public interface MapAccessorMapper { + + boolean supports(MapAccessor mapAccessor, Class targetClass); + + T map(MapAccessor mapAccessor, Class targetClass); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapperProvider.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapperProvider.java new file mode 100644 index 0000000000..4cc947dd54 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/MapAccessorMapperProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.util.List; +import java.util.Optional; +import org.neo4j.driver.types.MapAccessor; + +public class MapAccessorMapperProvider { + private static final List> mapAccessorMappers = + List.of(new ObjectMapper<>(), new DurationMapper()); + + @SuppressWarnings("unchecked") + public static Optional> mapper(MapAccessor mapAccessor, Class targetClass) { + return (Optional>) mapAccessorMappers.stream() + .filter(mapper -> mapper.supports(mapAccessor, targetClass)) + .findFirst(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectInstantiator.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectInstantiator.java new file mode 100644 index 0000000000..2cfd7f18ba --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectInstantiator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.util.List; +import org.neo4j.driver.exceptions.value.ValueException; + +class ObjectInstantiator { + + T instantiate(ObjectMetadata metadata) { + var constructor = metadata.constructor(); + var targetTypeName = constructor.getDeclaringClass().getCanonicalName(); + var initargs = initargs(targetTypeName, metadata.arguments()); + try { + return constructor.newInstance(initargs); + } catch (Throwable e) { + throw new ValueException("Failed to instantiate '%s'".formatted(targetTypeName), e); + } + } + + private Object[] initargs(String targetTypeName, List arguments) { + var initargs = new Object[arguments.size()]; + for (var i = 0; i < initargs.length; i++) { + var argument = arguments.get(i); + var type = argument.type(); + try { + initargs[i] = argument.value().as(type); + } catch (Throwable e) { + throw new ValueException( + "Failed to map '%s' property to '%s' for '%s' instantiation" + .formatted(argument.propertyName(), type.getTypeName(), targetTypeName), + e); + } + } + return initargs; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMapper.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMapper.java new file mode 100644 index 0000000000..c18d413b62 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.util.Set; +import org.neo4j.driver.Value; +import org.neo4j.driver.exceptions.value.ValueException; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.Type; +import org.neo4j.driver.types.TypeSystem; + +class ObjectMapper implements MapAccessorMapper { + private static final TypeSystem TS = TypeSystem.getDefault(); + + private static final Set SUPPORTED_VALUE_TYPES = Set.of(TS.MAP(), TS.NODE(), TS.RELATIONSHIP()); + + private static final ConstructorFinder CONSTRUCTOR_FINDER = new ConstructorFinder(); + + private static final ObjectInstantiator OBJECT_INSTANTIATOR = new ObjectInstantiator(); + + @Override + public boolean supports(MapAccessor mapAccessor, Class targetClass) { + return mapAccessor instanceof Value value + ? SUPPORTED_VALUE_TYPES.contains(value.type()) + : mapAccessor instanceof org.neo4j.driver.Record; + } + + @Override + public T map(MapAccessor mapAccessor, Class targetClass) { + return CONSTRUCTOR_FINDER + .findConstructor(mapAccessor, targetClass) + .map(OBJECT_INSTANTIATOR::instantiate) + .orElseThrow(() -> new ValueException( + "No suitable constructor has been found for '%s'".formatted(targetClass.getCanonicalName()))); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMetadata.java b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMetadata.java new file mode 100644 index 0000000000..9733464729 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/value/mapping/ObjectMetadata.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value.mapping; + +import java.lang.reflect.Constructor; +import java.util.List; + +record ObjectMetadata(Constructor constructor, List arguments) {} diff --git a/driver/src/main/java/org/neo4j/driver/mapping/Property.java b/driver/src/main/java/org/neo4j/driver/mapping/Property.java new file mode 100644 index 0000000000..78be893c0c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/mapping/Property.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.neo4j.driver.util.Preview; + +/** + * Defines property name that should be mapped to the annotated parameter. + * + * @since 5.28.5 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Preview(name = "Object mapping") +public @interface Property { + /** + * The property name that should be mapped to the annotated parameter. + * + * @return the property name + */ + String value(); +} diff --git a/driver/src/test/java/org/neo4j/driver/ObjectMappingTests.java b/driver/src/test/java/org/neo4j/driver/ObjectMappingTests.java new file mode 100644 index 0000000000..889db48ad0 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/ObjectMappingTests.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.Named; +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 org.neo4j.driver.internal.InternalIsoDuration; +import org.neo4j.driver.internal.InternalNode; +import org.neo4j.driver.internal.InternalPoint2D; +import org.neo4j.driver.internal.InternalPoint3D; +import org.neo4j.driver.internal.InternalRecord; +import org.neo4j.driver.internal.InternalRelationship; +import org.neo4j.driver.internal.value.NodeValue; +import org.neo4j.driver.internal.value.RelationshipValue; +import org.neo4j.driver.mapping.Property; +import org.neo4j.driver.types.IsoDuration; +import org.neo4j.driver.types.Point; + +class ObjectMappingTests { + @ParameterizedTest + @MethodSource("shouldMapValueArgs") + void shouldMapValue(Function, ValueHolder> valueFunction) { + // given + var string = "string"; + var listWithString = List.of(string, string); + var listWithStringValueHolder = List.of(new StringValueHolder(string), new StringValueHolder(string)); + var bytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + var bool = false; + var boltInteger = Long.MIN_VALUE; + var boltFloat = Double.MIN_VALUE; + var date = LocalDate.now(); + var time = OffsetTime.now(); + var dateTime = ZonedDateTime.now(); + var localDateTime = LocalDateTime.now(); + var duration = new InternalIsoDuration(Duration.ZERO); + var period = Period.ofYears(1000); + var javaDuration = Duration.of(1000, ChronoUnit.MINUTES); + var point2d = new InternalPoint2D(0, 0, 0); + var point3d = new InternalPoint3D(0, 0, 0, 0); + + var properties = Map.ofEntries( + Map.entry("string", Values.value(string)), + Map.entry("nullValue", Values.value((Object) null)), + Map.entry("listWithString", Values.value(listWithString)), + // also verifies MapValue + Map.entry( + "listWithStringValueHolder", + Values.value(listWithStringValueHolder.stream() + .map(v -> Map.of("string", Values.value(v.string()))) + .toList())), + Map.entry("bytes", Values.value(bytes)), + Map.entry("bool", Values.value(bool)), + Map.entry("boltInteger", Values.value(boltInteger)), + Map.entry("boltFloat", Values.value(boltFloat)), + Map.entry("date", Values.value(date)), + Map.entry("time", Values.value(time)), + Map.entry("dateTime", Values.value(dateTime)), + Map.entry("localDateTime", Values.value(localDateTime)), + Map.entry("duration", Values.value(duration)), + Map.entry("period", Values.value(period)), + Map.entry("javaDuration", Values.value(javaDuration)), + Map.entry("point2d", Values.value(point2d)), + Map.entry("point3d", Values.value(point3d))); + + // when + var valueHolder = valueFunction.apply(properties); + + // then + assertEquals(string, valueHolder.string()); + assertNull(valueHolder.nullValue()); + assertEquals(listWithString, valueHolder.listWithString()); + assertEquals(listWithStringValueHolder, valueHolder.listWithStringValueHolder()); + assertEquals(bytes, valueHolder.bytes()); + assertEquals(bool, valueHolder.bool()); + assertEquals(boltInteger, valueHolder.boltInteger()); + assertEquals(boltFloat, valueHolder.boltFloat()); + assertEquals(date, valueHolder.date()); + assertEquals(time, valueHolder.time()); + assertEquals(dateTime, valueHolder.dateTime()); + assertEquals(localDateTime, valueHolder.localDateTime()); + assertEquals(duration, valueHolder.duration()); + assertEquals(period, valueHolder.period()); + assertEquals(javaDuration, valueHolder.javaDuration()); + assertEquals(point2d, valueHolder.point2d()); + assertEquals(point3d, valueHolder.point3d()); + } + + static Stream shouldMapValueArgs() { + return Stream.of( + Arguments.of(Named., ValueHolder>>of( + "node", properties -> new NodeValue(new InternalNode(0L, Set.of("Product"), properties)) + .as(ValueHolder.class))), + Arguments.of(Named., ValueHolder>>of( + "relationship", + properties -> new RelationshipValue(new InternalRelationship(0L, 0L, 0L, "Product", properties)) + .as(ValueHolder.class))), + Arguments.of(Named., ValueHolder>>of( + "map", properties -> Values.value(properties).as(ValueHolder.class))), + Arguments.of(Named., ValueHolder>>of("record", properties -> { + var keys = new ArrayList(); + var values = new Value[properties.size()]; + var i = 0; + for (var entry : properties.entrySet()) { + keys.add(entry.getKey()); + values[i] = entry.getValue(); + i++; + } + return new InternalRecord(keys, values).as(ValueHolder.class); + }))); + } + + public record ValueHolder( + String string, + Object nullValue, + List listWithString, + List listWithStringValueHolder, + byte[] bytes, + boolean bool, + long boltInteger, + double boltFloat, + LocalDate date, + OffsetTime time, + ZonedDateTime dateTime, + LocalDateTime localDateTime, + IsoDuration duration, + Period period, + Duration javaDuration, + Point point2d, + Point point3d) {} + + public record StringValueHolder(String string) {} + + @Test + void shouldUseConstructorWithMaxMatchesAndMinMismatches() { + // given + var string = "string"; + var bytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + var bool = false; + + var properties = Map.ofEntries( + Map.entry("string", Values.value(string)), + Map.entry("bytes", Values.value(bytes)), + Map.entry("bool", Values.value(bool))); + + // when + var valueHolder = Values.value(properties).as(ValueHolderWithOptionalNumber.class); + + // then + assertEquals(string, valueHolder.string()); + assertEquals(bytes, valueHolder.bytes()); + assertEquals(bool, valueHolder.bool()); + assertEquals(Long.MIN_VALUE, valueHolder.number()); + } + + public record ValueHolderWithOptionalNumber(String string, byte[] bytes, boolean bool, long number) { + public ValueHolderWithOptionalNumber( + @Property("string") String string, @Property("bytes") byte[] bytes, @Property("bool") boolean bool) { + this(string, bytes, bool, Long.MIN_VALUE); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/BooleanValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/BooleanValueTest.java index a5af38c8fa..768181f167 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/BooleanValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/BooleanValueTest.java @@ -19,12 +19,18 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.internal.value.BooleanValue.FALSE; import static org.neo4j.driver.internal.value.BooleanValue.TRUE; +import java.io.Serializable; +import java.lang.constant.Constable; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; import org.neo4j.driver.types.TypeSystem; @@ -88,4 +94,15 @@ void shouldConvertToBooleanAndObject() { assertThat(TRUE.asObject(), equalTo((Object) Boolean.TRUE)); assertThat(FALSE.asObject(), equalTo((Object) Boolean.FALSE)); } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void shouldMapToType(boolean expected) { + var value = Values.value(expected); + assertEquals(expected, value.as(boolean.class)); + assertEquals(expected, value.as(Boolean.class)); + assertEquals(expected, value.as(Serializable.class)); + assertEquals(expected, value.as(Constable.class)); + assertEquals(expected, value.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/BytesValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/BytesValueTest.java index 7b389ea44c..ccbe7cee7f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/BytesValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/BytesValueTest.java @@ -19,10 +19,13 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; import org.neo4j.driver.types.TypeSystem; @@ -86,4 +89,12 @@ void shouldHaveBytesType() { InternalValue value = new BytesValue(TEST_BYTES); assertThat(value.type(), equalTo(InternalTypeSystem.TYPE_SYSTEM.BYTES())); } + + @Test + void shouldMapToType() { + var bytes = new byte[] {0}; + var values = Values.value(bytes); + assertEquals(bytes, values.as(byte[].class)); + assertNotNull(values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/DateTimeValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/DateTimeValueTest.java index 36832261e1..af6bf83837 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/DateTimeValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/DateTimeValueTest.java @@ -19,10 +19,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.chrono.ChronoZonedDateTime; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -66,4 +72,18 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, dateTimeValue::asLong); } + + @Test + void shouldMapToType() { + var date = ZonedDateTime.now(); + var values = Values.value(date); + assertEquals(date, values.as(ZonedDateTime.class)); + assertEquals(date, values.as(Temporal.class)); + assertEquals(date, values.as(TemporalAccessor.class)); + assertEquals(date, values.as(ChronoZonedDateTime.class)); + assertEquals(date, values.as(Comparable.class)); + assertEquals(date, values.as(Serializable.class)); + assertEquals(date, values.as(Object.class)); + assertEquals(date.toOffsetDateTime(), values.as(OffsetDateTime.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/DateValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/DateValueTest.java index e8e79eba95..09f78b3852 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/DateValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/DateValueTest.java @@ -19,8 +19,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; import java.time.LocalDate; +import java.time.chrono.ChronoLocalDate; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -53,4 +58,17 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, dateValue::asLong); } + + @Test + void shouldMapToType() { + var date = LocalDate.now(); + var values = Values.value(date); + assertEquals(date, values.as(LocalDate.class)); + assertEquals(date, values.as(Temporal.class)); + assertEquals(date, values.as(TemporalAdjuster.class)); + assertEquals(date, values.as(ChronoLocalDate.class)); + assertEquals(date, values.as(Comparable.class)); + assertEquals(date, values.as(Serializable.class)); + assertEquals(date, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/DurationValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/DurationValueTest.java index e8412ef8ef..2790c0863e 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/DurationValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/DurationValueTest.java @@ -18,8 +18,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import java.time.temporal.TemporalAmount; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.InternalIsoDuration; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -55,6 +58,15 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, durationValue::asLong); } + @Test + void shouldMapToType() { + var date = mock(IsoDuration.class); + var values = Values.value(date); + assertEquals(date, values.as(IsoDuration.class)); + assertEquals(date, values.as(TemporalAmount.class)); + assertEquals(date, values.as(Object.class)); + } + private static IsoDuration newDuration(long months, long days, long seconds, int nanoseconds) { return new InternalIsoDuration(months, days, seconds, nanoseconds); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/FloatValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/FloatValueTest.java index 51e5fbc2ca..1a55e1493e 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/FloatValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/FloatValueTest.java @@ -19,11 +19,16 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.LossyCoercion; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; @@ -117,4 +122,25 @@ void shouldThrowIfSmallerThanIntegerMin() { assertThat(value1.asInt(), equalTo(Integer.MIN_VALUE)); assertThrows(LossyCoercion.class, value2::asInt); } + + @SuppressWarnings("ConstantValue") + @Test + void shouldMapToType() { + var expected = 0.0; + var value = Values.value(expected); + assertEquals(expected, value.as(double.class)); + assertEquals(expected, value.as(Double.class)); + assertEquals(expected, value.as(Number.class)); + assertEquals(expected, value.as(Serializable.class)); + assertEquals(expected, value.as(Comparable.class)); + assertEquals(expected, value.as(Constable.class)); + assertEquals(expected, value.as(ConstantDesc.class)); + assertEquals(expected, value.as(Object.class)); + assertEquals((long) expected, value.as(long.class)); + assertEquals((long) expected, value.as(Long.class)); + assertEquals((int) expected, value.as(int.class)); + assertEquals((int) expected, value.as(Integer.class)); + assertEquals((float) expected, value.as(float.class)); + assertEquals((float) expected, value.as(Float.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java index 9ffb75f381..6403b602f3 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java @@ -19,11 +19,16 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.LossyCoercion; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; @@ -124,4 +129,24 @@ void shouldThrowIfLargerThan() { assertThat(value1.asDouble(), equalTo(9007199254740992D)); assertThrows(LossyCoercion.class, value2::asDouble); } + + @Test + void shouldMapToType() { + var expected = 0L; + var value = Values.value(expected); + assertEquals(expected, value.as(long.class)); + assertEquals(expected, value.as(Long.class)); + assertEquals(expected, value.as(Number.class)); + assertEquals(expected, value.as(Serializable.class)); + assertEquals(expected, value.as(Comparable.class)); + assertEquals(expected, value.as(Constable.class)); + assertEquals(expected, value.as(ConstantDesc.class)); + assertEquals((int) expected, value.as(int.class)); + assertEquals((int) expected, value.as(Integer.class)); + assertEquals(expected, value.as(Object.class)); + assertEquals(expected, value.as(double.class)); + assertEquals(expected, value.as(Double.class)); + assertEquals((float) expected, value.as(float.class)); + assertEquals((float) expected, value.as(Float.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java index 01c25169fb..97e5be2e80 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java @@ -18,10 +18,14 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.neo4j.driver.Values.value; +import java.util.Collection; +import java.util.List; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; class ListValueTest { @@ -39,6 +43,16 @@ void shouldHaveCorrectType() { assertThat(listValue.type(), equalTo(InternalTypeSystem.TYPE_SYSTEM.LIST())); } + @Test + void shouldMapToType() { + var list = List.of(0L); + var values = Values.value(list); + assertEquals(list, values.as(List.class)); + assertEquals(list, values.as(Collection.class)); + assertEquals(list, values.as(Iterable.class)); + assertEquals(list, values.as(Object.class)); + } + private ListValue listValue(Value... values) { return new ListValue(values); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/LocalDateTimeValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/LocalDateTimeValueTest.java index b3ec2af499..167b846e46 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/LocalDateTimeValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/LocalDateTimeValueTest.java @@ -22,8 +22,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; import java.time.LocalDateTime; +import java.time.chrono.ChronoLocalDateTime; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -56,4 +61,17 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, dateTimeValue::asLong); } + + @Test + void shouldMapToType() { + var date = LocalDateTime.now(); + var values = Values.value(date); + assertEquals(date, values.as(LocalDateTime.class)); + assertEquals(date, values.as(Temporal.class)); + assertEquals(date, values.as(TemporalAdjuster.class)); + assertEquals(date, values.as(ChronoLocalDateTime.class)); + assertEquals(date, values.as(Comparable.class)); + assertEquals(date, values.as(Serializable.class)); + assertEquals(date, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/LocalTimeValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/LocalTimeValueTest.java index cb2b8b3265..44d4fc0eca 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/LocalTimeValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/LocalTimeValueTest.java @@ -19,8 +19,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; import java.time.LocalTime; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -53,4 +57,16 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, timeValue::asLong); } + + @Test + void shouldMapToType() { + var date = LocalTime.now(); + var values = Values.value(date); + assertEquals(date, values.as(LocalTime.class)); + assertEquals(date, values.as(Temporal.class)); + assertEquals(date, values.as(TemporalAdjuster.class)); + assertEquals(date, values.as(Comparable.class)); + assertEquals(date, values.as(Serializable.class)); + assertEquals(date, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/MapValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/MapValueTest.java index fb902c44df..a9bbe33cdd 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/MapValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/MapValueTest.java @@ -18,12 +18,15 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.neo4j.driver.Values.value; import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; class MapValueTest { @@ -54,6 +57,14 @@ void shouldNotBeNull() { assertFalse(map.isNull()); } + @Test + void shouldMapToType() { + var map = Map.of("key", "value"); + var values = Values.value(map); + assertEquals(map, values.as(Map.class)); + assertEquals(map, values.as(Object.class)); + } + private MapValue mapValue() { var map = new HashMap(); map.put("k1", value("v1")); diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/NodeValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/NodeValueTest.java index cbee4ade8b..5cced72349 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/NodeValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/NodeValueTest.java @@ -24,8 +24,13 @@ import static org.neo4j.driver.internal.util.ValueFactory.filledNodeValue; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; +import org.neo4j.driver.types.Entity; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.Node; class NodeValueTest { @Test @@ -55,4 +60,14 @@ void shouldTypeAsNode() { InternalValue value = emptyNodeValue(); assertThat(value.typeConstructor(), equalTo(TypeConstructor.NODE)); } + + @Test + void shouldMapToType() { + var node = new InternalNode(0); + var values = Values.value(node); + assertEquals(node, values.as(Node.class)); + assertEquals(node, values.as(Entity.class)); + assertEquals(node, values.as(MapAccessor.class)); + assertEquals(node, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/NullValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/NullValueTest.java index 75ee6fafd0..cb00e833e3 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/NullValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/NullValueTest.java @@ -20,6 +20,7 @@ import static java.util.Collections.emptyMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.Values.isoDuration; import static org.neo4j.driver.Values.ofValue; @@ -34,6 +35,7 @@ import java.util.function.Function; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.TypeConstructor; class NullValueTest { @@ -120,6 +122,14 @@ void shouldReturnAsDefaultValue() { assertComputeOrDefaultReturnDefault(Value::asNumber, 10); } + @Test + void shouldMapToType() { + var values = Values.value((Object) null); + // unlike asString(), this returns null + assertNull(values.as(String.class)); + assertNull(values.as(Object.class)); + } + private static void assertComputeOrDefaultReturnDefault(Function f, T defaultAndExpectedValue) { var value = NullValue.NULL; assertThat(value.computeOrDefault(f, defaultAndExpectedValue), equalTo(defaultAndExpectedValue)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/PathValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/PathValueTest.java index 8d445869e7..eb2d4058b3 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/PathValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/PathValueTest.java @@ -24,7 +24,12 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.InternalNode; +import org.neo4j.driver.internal.InternalPath; +import org.neo4j.driver.internal.InternalRelationship; import org.neo4j.driver.internal.types.InternalTypeSystem; +import org.neo4j.driver.types.Path; class PathValueTest { @Test @@ -42,4 +47,14 @@ void shouldNotBeNull() { void shouldHaveCorrectType() { assertThat(filledPathValue().type(), equalTo(InternalTypeSystem.TYPE_SYSTEM.PATH())); } + + @Test + void shouldMapToType() { + var path = new InternalPath( + new InternalNode(42L), new InternalRelationship(43L, 42L, 44L, "T"), new InternalNode(44L)); + var values = Values.value(path); + assertEquals(path, values.as(Path.class)); + assertEquals(path, values.as(Iterable.class)); + assertEquals(path, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/PointValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/PointValueTest.java new file mode 100644 index 0000000000..0f17503204 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/value/PointValueTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.value; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; +import org.neo4j.driver.types.Point; + +class PointValueTest { + + @Test + void shouldMapToType() { + var point = mock(Point.class); + var values = Values.value(point); + assertEquals(point, values.as(Point.class)); + assertEquals(point, values.as(Object.class)); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/RelationshipValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/RelationshipValueTest.java index 1d871c2e84..c0bcb5f2a6 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/RelationshipValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/RelationshipValueTest.java @@ -25,7 +25,12 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.InternalRelationship; import org.neo4j.driver.internal.types.TypeConstructor; +import org.neo4j.driver.types.Entity; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.Relationship; class RelationshipValueTest { @Test @@ -51,4 +56,14 @@ void shouldTypeAsRelationship() { InternalValue value = emptyRelationshipValue(); assertThat(value.typeConstructor(), equalTo(TypeConstructor.RELATIONSHIP)); } + + @Test + void shouldMapToType() { + var relationship = new InternalRelationship(0, 0, 0, "value"); + var values = Values.value(relationship); + assertEquals(relationship, values.as(Relationship.class)); + assertEquals(relationship, values.as(Entity.class)); + assertEquals(relationship, values.as(MapAccessor.class)); + assertEquals(relationship, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java index d353ed4c25..7dd5df8ea3 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java @@ -19,10 +19,15 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import java.io.Serializable; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.Values; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.types.TypeConstructor; import org.neo4j.driver.types.TypeSystem; @@ -84,4 +89,17 @@ void shouldHaveStringType() { InternalValue value = new StringValue("Spongebob"); assertThat(value.type(), equalTo(InternalTypeSystem.TYPE_SYSTEM.STRING())); } + + @Test + void shouldMapToType() { + var string = "value"; + var values = Values.value(string); + assertEquals(string, values.as(String.class)); + assertEquals(string, values.as(Serializable.class)); + assertEquals(string, values.as(Comparable.class)); + assertEquals(string, values.as(CharSequence.class)); + assertEquals(string, values.as(Constable.class)); + assertEquals(string, values.as(ConstantDesc.class)); + assertEquals(string, values.as(Object.class)); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/TimeValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/TimeValueTest.java index 155e547096..7860b44063 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/TimeValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/TimeValueTest.java @@ -19,9 +19,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.Serializable; import java.time.OffsetTime; import java.time.ZoneOffset; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; @@ -54,4 +58,16 @@ void shouldNotSupportAsLong() { assertThrows(Uncoercible.class, timeValue::asLong); } + + @Test + void shouldMapToType() { + var date = OffsetTime.now(); + var values = Values.value(date); + assertEquals(date, values.as(OffsetTime.class)); + assertEquals(date, values.as(Temporal.class)); + assertEquals(date, values.as(TemporalAdjuster.class)); + assertEquals(date, values.as(Comparable.class)); + assertEquals(date, values.as(Serializable.class)); + assertEquals(date, values.as(Object.class)); + } }