Skip to content

Commit 9476626

Browse files
committed
feat(object-mapping): Add support for mapping LIST Value to arrays
1 parent 65b187c commit 9476626

File tree

8 files changed

+155
-4
lines changed

8 files changed

+155
-4
lines changed

driver/src/main/java/org/neo4j/driver/Record.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ public interface Record extends MapAccessorWithDefaultValue {
129129
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
130130
* available.
131131
* <p>
132+
* The mapping only works for types with directly accessible constructors, not interfaces or abstract types.
133+
* <p>
132134
* Types with generic parameters defined at the class level are not supported. However, constructor arguments with
133135
* specific types are permitted, see the {@code actors} parameter in the example above.
134136
* <p>

driver/src/main/java/org/neo4j/driver/Value.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,11 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
545545
* </tr>
546546
* <tr>
547547
* <td>{@link TypeSystem#STRING}</td>
548-
* <td>{@link String}</td>
548+
* <td>{@link String}, {@code char}, {@link Character}</td>
549549
* </tr>
550550
* <tr>
551551
* <td>{@link TypeSystem#INTEGER}</td>
552-
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
552+
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code short}, {@link Short}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
553553
* </tr>
554554
* <tr>
555555
* <td>{@link TypeSystem#FLOAT}</td>
@@ -594,7 +594,9 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
594594
* </tr>
595595
* <tr>
596596
* <td>{@link TypeSystem#LIST}</td>
597-
* <td>{@link List}</td>
597+
* <td>{@link List}, {@code T[]} as long as list elements may be mapped to the array component type
598+
* (for example, {@code char[]}, {@code boolean[]}, {@code String[]}, {@code long[]}, {@code int[]},
599+
* {@code short[]}, {@code double[]}, {@code float[]})</td>
598600
* </tr>
599601
* <tr>
600602
* <td>{@link TypeSystem#MAP}</td>
@@ -663,6 +665,8 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
663665
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
664666
* available.
665667
* <p>
668+
* The mapping only works for types with directly accessible constructors, not interfaces or abstract types.
669+
* <p>
666670
* Example with optional property (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
667671
* <pre>
668672
* {@code

driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,21 @@ public <T> T as(Class<T> targetClass) {
8585
return (T) Float.valueOf(asFloat());
8686
} else if (targetClass.equals(Float.class)) {
8787
return targetClass.cast(asFloat());
88+
} else if (targetClass.equals(short.class)) {
89+
return (T) Short.valueOf(asShort());
90+
} else if (targetClass.equals(Short.class)) {
91+
return targetClass.cast(asShort());
8892
}
8993
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
9094
}
9195

96+
private short asShort() {
97+
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
98+
throw new LossyCoercion(type().name(), "Java short");
99+
}
100+
return (short) val;
101+
}
102+
92103
@Override
93104
public String toString() {
94105
return Long.toString(val);

driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

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

21+
import java.lang.reflect.Array;
2122
import java.lang.reflect.ParameterizedType;
2223
import java.util.Arrays;
2324
import java.util.Iterator;
@@ -26,6 +27,7 @@
2627
import org.neo4j.driver.Value;
2728
import org.neo4j.driver.Values;
2829
import org.neo4j.driver.exceptions.value.Uncoercible;
30+
import org.neo4j.driver.exceptions.value.ValueException;
2931
import org.neo4j.driver.internal.types.InternalTypeSystem;
3032
import org.neo4j.driver.internal.util.Extract;
3133
import org.neo4j.driver.types.Type;
@@ -64,6 +66,22 @@ public <T> List<T> asList(Function<Value, T> mapFunction) {
6466
public <T> T as(Class<T> targetClass) {
6567
if (targetClass.isAssignableFrom(List.class)) {
6668
return targetClass.cast(asList());
69+
} else if (targetClass.isArray()) {
70+
var componentType = targetClass.componentType();
71+
var array = Array.newInstance(componentType, values.length);
72+
for (var i = 0; i < values.length; i++) {
73+
Object value;
74+
try {
75+
value = values[i].as(componentType);
76+
} catch (Throwable throwable) {
77+
throw new ValueException(
78+
"Failed to map LIST value to %s - an error occured while mapping the element at index %d"
79+
.formatted(targetClass.getCanonicalName(), i),
80+
throwable);
81+
}
82+
Array.set(array, i, value);
83+
}
84+
return targetClass.cast(array);
6785
}
6886
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
6987
}

driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.neo4j.driver.internal.value;
1818

1919
import java.util.Objects;
20+
import org.neo4j.driver.exceptions.value.LossyCoercion;
2021
import org.neo4j.driver.exceptions.value.Uncoercible;
2122
import org.neo4j.driver.internal.types.InternalTypeSystem;
2223
import org.neo4j.driver.types.Type;
@@ -51,10 +52,17 @@ public String asString() {
5152
return val;
5253
}
5354

55+
@SuppressWarnings("unchecked")
5456
@Override
5557
public <T> T as(Class<T> targetClass) {
5658
if (targetClass.isAssignableFrom(String.class)) {
5759
return targetClass.cast(asString());
60+
} else if ((targetClass.isAssignableFrom(char.class) || targetClass.isAssignableFrom(Character.class))) {
61+
if (val.length() == 1) {
62+
return (T) Character.valueOf(val.charAt(0));
63+
} else {
64+
throw new LossyCoercion(type().name(), targetClass.getCanonicalName());
65+
}
5866
}
5967
throw new Uncoercible(type().name(), targetClass.getCanonicalName());
6068
}

driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,7 @@ void shouldMapToType() {
148148
assertEquals(expected, value.as(Double.class));
149149
assertEquals((float) expected, value.as(float.class));
150150
assertEquals((float) expected, value.as(Float.class));
151+
assertEquals((short) expected, value.as(short.class));
152+
assertEquals((short) expected, value.as(Short.class));
151153
}
152154
}

driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818

1919
import static org.hamcrest.CoreMatchers.equalTo;
2020
import static org.hamcrest.MatcherAssert.assertThat;
21+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2122
import static org.junit.jupiter.api.Assertions.assertEquals;
2223
import static org.neo4j.driver.Values.value;
2324

25+
import java.time.LocalDateTime;
26+
import java.util.Arrays;
2427
import java.util.Collection;
2528
import java.util.List;
2629
import org.junit.jupiter.api.Test;
@@ -53,6 +56,107 @@ void shouldMapToType() {
5356
assertEquals(list, values.as(Object.class));
5457
}
5558

59+
@Test
60+
void shouldMapCharValuesToArray() {
61+
var array = new char[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
62+
var values = Values.value(array);
63+
assertArrayEquals(array, values.as(char[].class));
64+
}
65+
66+
@Test
67+
void shouldMapBooleanValuesToArray() {
68+
var array = new boolean[] {false};
69+
var values = Values.value(array);
70+
assertArrayEquals(array, values.as(boolean[].class));
71+
}
72+
73+
@Test
74+
void shouldMapStringValuesToArray() {
75+
var array = new String[] {"value"};
76+
var values = Values.value(array);
77+
assertArrayEquals(array, values.as(String[].class));
78+
}
79+
80+
@Test
81+
void shouldMapLongValuesToArray() {
82+
var array = new long[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
83+
var values = Values.value(array);
84+
assertArrayEquals(array, values.as(long[].class));
85+
assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class));
86+
assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class));
87+
var expectedFloats = new float[array.length];
88+
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
89+
assertArrayEquals(expectedFloats, values.as(float[].class));
90+
}
91+
92+
@Test
93+
void shouldMapIntegerValuesToArray() {
94+
var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
95+
var values = Values.value(array);
96+
assertArrayEquals(array, values.as(int[].class));
97+
assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class));
98+
assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class));
99+
var expectedFloats = new float[array.length];
100+
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
101+
assertArrayEquals(expectedFloats, values.as(float[].class));
102+
}
103+
104+
@Test
105+
void shouldMapShortValuesToArray() {
106+
var array = new short[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
107+
var values = Values.value(array);
108+
assertArrayEquals(array, values.as(short[].class));
109+
}
110+
111+
@Test
112+
void shouldMapDoubleValuesToArray() {
113+
var array = new double[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
114+
var values = Values.value(array);
115+
assertArrayEquals(array, values.as(double[].class));
116+
assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class));
117+
assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class));
118+
var expectedFloats = new float[array.length];
119+
for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i];
120+
assertArrayEquals(expectedFloats, values.as(float[].class));
121+
}
122+
123+
@Test
124+
void shouldMapFloatValuesToArray() {
125+
var array = new float[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
126+
var values = Values.value(array);
127+
assertArrayEquals(array, values.as(float[].class));
128+
var expectedDoubles = new double[array.length];
129+
for (var i = 0; i < array.length; i++) expectedDoubles[i] = array[i];
130+
assertArrayEquals(expectedDoubles, values.as(double[].class));
131+
var expectedLongs = new long[array.length];
132+
for (var i = 0; i < array.length; i++) expectedLongs[i] = (long) array[i];
133+
assertArrayEquals(expectedLongs, values.as(long[].class));
134+
var expectedIntegers = new int[array.length];
135+
for (var i = 0; i < array.length; i++) expectedIntegers[i] = (int) array[i];
136+
assertArrayEquals(expectedIntegers, values.as(int[].class));
137+
}
138+
139+
@Test
140+
void shouldMapObjectsToArray() {
141+
var string = "value";
142+
var localDateTime = LocalDateTime.now();
143+
var array = new Object[] {string, localDateTime};
144+
var values = Values.value(array);
145+
assertArrayEquals(array, values.as(Object[].class));
146+
}
147+
148+
@Test
149+
void shouldMapMatrixValuesToArrays() {
150+
var array = new long[10][10];
151+
for (var i = 0; i < array.length; i++) {
152+
for (var j = 0; j < 10; j++) {
153+
array[i][j] = i * j;
154+
}
155+
}
156+
var values = Values.value(array);
157+
assertArrayEquals(array, values.as(long[][].class));
158+
}
159+
56160
private ListValue listValue(Value... values) {
57161
return new ListValue(values);
58162
}

driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ void shouldHaveStringType() {
9292

9393
@Test
9494
void shouldMapToType() {
95-
var string = "value";
95+
var string = "v";
9696
var values = Values.value(string);
9797
assertEquals(string, values.as(String.class));
98+
assertEquals(string.charAt(0), values.as(char.class));
99+
assertEquals(string.charAt(0), values.as(Character.class));
98100
assertEquals(string, values.as(Serializable.class));
99101
assertEquals(string, values.as(Comparable.class));
100102
assertEquals(string, values.as(CharSequence.class));

0 commit comments

Comments
 (0)