Skip to content

feat(object-mapping): Add support for mapping list value to array #1637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions driver/src/main/java/org/neo4j/driver/Record.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public interface Record extends MapAccessorWithDefaultValue {
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
* available.
* <p>
* The mapping only works for types with directly accessible constructors, not interfaces or abstract types.
* <p>
* 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.
* <p>
Expand Down
10 changes: 7 additions & 3 deletions driver/src/main/java/org/neo4j/driver/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -545,11 +545,11 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
* </tr>
* <tr>
* <td>{@link TypeSystem#STRING}</td>
* <td>{@link String}</td>
* <td>{@link String}, {@code char}, {@link Character}</td>
* </tr>
* <tr>
* <td>{@link TypeSystem#INTEGER}</td>
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code short}, {@link Short}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
* </tr>
* <tr>
* <td>{@link TypeSystem#FLOAT}</td>
Expand Down Expand Up @@ -594,7 +594,9 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
* </tr>
* <tr>
* <td>{@link TypeSystem#LIST}</td>
* <td>{@link List}</td>
* <td>{@link List}, {@code T[]} as long as list elements may be mapped to the array component type
* (for example, {@code char[]}, {@code boolean[]}, {@code String[]}, {@code long[]}, {@code int[]},
* {@code short[]}, {@code double[]}, {@code float[]})</td>
* </tr>
* <tr>
* <td>{@link TypeSystem#MAP}</td>
Expand Down Expand Up @@ -663,6 +665,8 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
* available.
* <p>
* The mapping only works for types with directly accessible constructors, not interfaces or abstract types.
* <p>
* Example with optional property (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
* <pre>
* {@code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,21 @@ public <T> T as(Class<T> targetClass) {
return (T) Float.valueOf(asFloat());
} else if (targetClass.equals(Float.class)) {
return targetClass.cast(asFloat());
} else if (targetClass.equals(short.class)) {
return (T) Short.valueOf(asShort());
} else if (targetClass.equals(Short.class)) {
return targetClass.cast(asShort());
}
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
}

private short asShort() {
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
throw new LossyCoercion(type().name(), "Java short");
}
return (short) val;
}

@Override
public String toString() {
return Long.toString(val);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static org.neo4j.driver.Values.ofObject;

import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Iterator;
Expand All @@ -26,6 +27,7 @@
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.exceptions.value.Uncoercible;
import org.neo4j.driver.exceptions.value.ValueException;
import org.neo4j.driver.internal.types.InternalTypeSystem;
import org.neo4j.driver.internal.util.Extract;
import org.neo4j.driver.types.Type;
Expand Down Expand Up @@ -64,6 +66,22 @@ public <T> List<T> asList(Function<Value, T> mapFunction) {
public <T> T as(Class<T> targetClass) {
if (targetClass.isAssignableFrom(List.class)) {
return targetClass.cast(asList());
} else if (targetClass.isArray()) {
var componentType = targetClass.componentType();
var array = Array.newInstance(componentType, values.length);
for (var i = 0; i < values.length; i++) {
Object value;
try {
value = values[i].as(componentType);
} catch (Throwable throwable) {
throw new ValueException(
"Failed to map LIST value to %s - an error occured while mapping the element at index %d"
.formatted(targetClass.getCanonicalName(), i),
throwable);
}
Array.set(array, i, value);
}
return targetClass.cast(array);
}
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.neo4j.driver.internal.value;

import java.util.Objects;
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;
Expand Down Expand Up @@ -51,10 +52,17 @@ public String asString() {
return val;
}

@SuppressWarnings("unchecked")
@Override
public <T> T as(Class<T> targetClass) {
if (targetClass.isAssignableFrom(String.class)) {
return targetClass.cast(asString());
} else if ((targetClass.isAssignableFrom(char.class) || targetClass.isAssignableFrom(Character.class))) {
if (val.length() == 1) {
return (T) Character.valueOf(val.charAt(0));
} else {
throw new LossyCoercion(type().name(), targetClass.getCanonicalName());
}
}
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,7 @@ void shouldMapToType() {
assertEquals(expected, value.as(Double.class));
assertEquals((float) expected, value.as(float.class));
assertEquals((float) expected, value.as(Float.class));
assertEquals((short) expected, value.as(short.class));
assertEquals((short) expected, value.as(Short.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.neo4j.driver.Values.value;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -53,6 +56,107 @@ void shouldMapToType() {
assertEquals(list, values.as(Object.class));
}

@Test
void shouldMapCharValuesToArray() {
var array = new char[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(char[].class));
}

@Test
void shouldMapBooleanValuesToArray() {
var array = new boolean[] {false};
var values = Values.value(array);
assertArrayEquals(array, values.as(boolean[].class));
}

@Test
void shouldMapStringValuesToArray() {
var array = new String[] {"value"};
var values = Values.value(array);
assertArrayEquals(array, values.as(String[].class));
}

@Test
void shouldMapLongValuesToArray() {
var array = new long[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(long[].class));
assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class));
assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class));
var expectedFloats = new float[array.length];
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
assertArrayEquals(expectedFloats, values.as(float[].class));
}

@Test
void shouldMapIntegerValuesToArray() {
var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(int[].class));
assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class));
assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class));
var expectedFloats = new float[array.length];
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
assertArrayEquals(expectedFloats, values.as(float[].class));
}

@Test
void shouldMapShortValuesToArray() {
var array = new short[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(short[].class));
}

@Test
void shouldMapDoubleValuesToArray() {
var array = new double[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(double[].class));
assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class));
assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class));
var expectedFloats = new float[array.length];
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
assertArrayEquals(expectedFloats, values.as(float[].class));
}

@Test
void shouldMapFloatValuesToArray() {
var array = new float[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var values = Values.value(array);
assertArrayEquals(array, values.as(float[].class));
var expectedDoubles = new double[array.length];
for (var i = 0; i < array.length; i++) expectedDoubles[i] = array[i];
assertArrayEquals(expectedDoubles, values.as(double[].class));
var expectedLongs = new long[array.length];
for (var i = 0; i < array.length; i++) expectedLongs[i] = (long) array[i];
assertArrayEquals(expectedLongs, values.as(long[].class));
var expectedIntegers = new int[array.length];
for (var i = 0; i < array.length; i++) expectedIntegers[i] = (int) array[i];
assertArrayEquals(expectedIntegers, values.as(int[].class));
}

@Test
void shouldMapObjectsToArray() {
var string = "value";
var localDateTime = LocalDateTime.now();
var array = new Object[] {string, localDateTime};
var values = Values.value(array);
assertArrayEquals(array, values.as(Object[].class));
}

@Test
void shouldMapMatrixValuesToArrays() {
var array = new long[10][10];
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < 10; j++) {
array[i][j] = i * j;
}
}
var values = Values.value(array);
assertArrayEquals(array, values.as(long[][].class));
}

private ListValue listValue(Value... values) {
return new ListValue(values);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ void shouldHaveStringType() {

@Test
void shouldMapToType() {
var string = "value";
var string = "0";
var values = Values.value(string);
assertEquals(string, values.as(String.class));
assertEquals(string.charAt(0), values.as(char.class));
assertEquals(string.charAt(0), values.as(Character.class));
assertEquals(string, values.as(Serializable.class));
assertEquals(string, values.as(Comparable.class));
assertEquals(string, values.as(CharSequence.class));
Expand Down