Skip to content

Commit 3995b42

Browse files
committed
Add @Elements converter
1 parent ad4c085 commit 3995b42

4 files changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright © 2025-present Stefano Cordio
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.scordio.junit.converters;
17+
18+
import org.junit.jupiter.params.converter.ConvertWith;
19+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* {@code @Elements} is a {@link ConvertWith} composed annotation that converts
28+
* collection-like string representations to JDK collection types.
29+
* <p>
30+
* The following target types are supported:
31+
*
32+
* <ul>
33+
* <li>One-dimensional {@code int}, {@code long}, and {@code double} arrays</li>
34+
* <li>One-dimensional primitive and object arrays &mdash; for example {@code Integer[]},
35+
* {@code String[]}, etc.</li>
36+
* <li>{@link java.util.Collection}</li>
37+
* <li>{@link java.util.List}</li>
38+
* <li>{@link java.util.Set}</li>
39+
* </ul>
40+
*
41+
* <p>
42+
* The elements should be separated by a comma, and each element is converted using
43+
* JUnit's {@link org.junit.platform.commons.support.conversion.ConversionSupport
44+
* conversion capabilities}.
45+
*
46+
* @see org.junit.platform.commons.support.conversion.ConversionSupport
47+
*/
48+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD })
49+
@Retention(RetentionPolicy.RUNTIME)
50+
@Documented
51+
@ConvertWith(ElementsArgumentConverter.class)
52+
@SuppressWarnings("exports")
53+
public @interface Elements {
54+
55+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright © 2025-present Stefano Cordio
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.scordio.junit.converters;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.junit.jupiter.api.extension.ParameterContext;
20+
import org.junit.jupiter.params.converter.ArgumentConversionException;
21+
import org.junit.jupiter.params.converter.ArgumentConverter;
22+
import org.junit.jupiter.params.support.FieldContext;
23+
import org.junit.platform.commons.support.conversion.ConversionSupport;
24+
25+
import java.lang.reflect.Array;
26+
import java.lang.reflect.ParameterizedType;
27+
import java.lang.reflect.Type;
28+
import java.util.Arrays;
29+
import java.util.Collection;
30+
import java.util.List;
31+
import java.util.Objects;
32+
import java.util.Set;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
36+
class ElementsArgumentConverter implements ArgumentConverter {
37+
38+
@Override
39+
public final Object convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException {
40+
return doConvert(source, context.getParameter().getParameterizedType());
41+
}
42+
43+
@Override
44+
public final Object convert(@Nullable Object source, FieldContext context) throws ArgumentConversionException {
45+
return doConvert(source, context.getField().getGenericType());
46+
}
47+
48+
private Object doConvert(@Nullable Object source, Type targetType) throws ArgumentConversionException {
49+
Objects.requireNonNull(source, "'null' is not supported");
50+
51+
if (!(source instanceof String)) {
52+
throw new ArgumentConversionException(
53+
String.format("Source type %s is not supported", source.getClass().getTypeName()));
54+
}
55+
56+
String[] elements = ((String) source).split(",");
57+
58+
Stream<String> stream = Arrays.stream(elements).map(String::trim);
59+
60+
if (targetType instanceof Class) {
61+
Class<?> clazz = (Class<?>) targetType;
62+
if (clazz.isArray()) {
63+
return convertArray(stream, clazz.getComponentType());
64+
}
65+
}
66+
if (targetType instanceof ParameterizedType) {
67+
return convertCollection(stream, (ParameterizedType) targetType);
68+
}
69+
70+
throw new ArgumentConversionException(
71+
String.format("Target type %s is not supported", targetType.getTypeName()));
72+
}
73+
74+
private static Object convertArray(Stream<String> stream, Class<?> componentType) {
75+
List<String> strings = stream.collect(Collectors.toList());
76+
Object array = Array.newInstance(componentType, strings.size());
77+
for (int i = 0; i < strings.size(); i++) {
78+
Array.set(array, i, ConversionSupport.convert(strings.get(i), componentType, null));
79+
}
80+
return array;
81+
}
82+
83+
private static Collection<?> convertCollection(Stream<String> stream, ParameterizedType parameterizedType) {
84+
Type rawType = parameterizedType.getRawType();
85+
Class<?> elementType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
86+
Stream<?> convertedStream = stream.map(value -> ConversionSupport.convert(value, elementType, null));
87+
88+
if (rawType == List.class || rawType == Collection.class || rawType == Iterable.class) {
89+
return convertedStream.collect(Collectors.toList());
90+
}
91+
if (rawType == Set.class) {
92+
return convertedStream.collect(Collectors.toSet());
93+
}
94+
throw new IllegalArgumentException(
95+
String.format("Target type %s is not supported", parameterizedType.getTypeName()));
96+
}
97+
98+
}

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
requires static spring.core;
3030

3131
requires org.junit.jupiter.params;
32+
requires org.junit.platform.commons;
3233

3334
exports io.github.scordio.junit.converters;
3435

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright © 2025-present Stefano Cordio
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.scordio.tests.junit.converters;
17+
18+
import io.github.scordio.junit.converters.Elements;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.FieldSource;
22+
23+
import java.util.Collection;
24+
import java.util.List;
25+
import java.util.Set;
26+
27+
import static io.github.scordio.tests.junit.converters.JupiterEngineTestKit.executeTestsForClass;
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.junit.jupiter.params.provider.Arguments.arguments;
30+
31+
class ElementsIntegrationTests {
32+
33+
@Test
34+
void should_convert_supported_values() {
35+
executeTestsForClass(SupportedValuesTestCase.class).testEvents()
36+
.assertStatistics(stats -> stats.started(16).succeeded(16));
37+
}
38+
39+
static class SupportedValuesTestCase {
40+
41+
@ParameterizedTest
42+
@FieldSource
43+
void array_of_primitive_booleans(@Elements boolean[] elements, boolean[] expected) {
44+
assertThat(elements).isEqualTo(expected);
45+
}
46+
47+
static List<?> array_of_primitive_booleans = List.of(arguments("true, false", new boolean[] { true, false }));
48+
49+
@ParameterizedTest
50+
@FieldSource
51+
void array_of_primitive_chars(@Elements char[] elements, char[] expected) {
52+
assertThat(elements).isEqualTo(expected);
53+
}
54+
55+
static List<?> array_of_primitive_chars = List.of(arguments("a, b, c", new char[] { 'a', 'b', 'c' }));
56+
57+
@ParameterizedTest
58+
@FieldSource
59+
void array_of_primitive_bytes(@Elements byte[] elements, byte[] expected) {
60+
assertThat(elements).isEqualTo(expected);
61+
}
62+
63+
static List<?> array_of_primitive_bytes = List.of(arguments("1, 2, 3", new byte[] { 1, 2, 3 }));
64+
65+
@ParameterizedTest
66+
@FieldSource
67+
void array_of_primitive_shorts(@Elements short[] elements, short[] expected) {
68+
assertThat(elements).isEqualTo(expected);
69+
}
70+
71+
static List<?> array_of_primitive_shorts = List.of(arguments("1, 2, 3", new short[] { 1, 2, 3 }));
72+
73+
@ParameterizedTest
74+
@FieldSource
75+
void array_of_primitive_integers(@Elements int[] elements, int[] expected) {
76+
assertThat(elements).isEqualTo(expected);
77+
}
78+
79+
static List<?> array_of_primitive_integers = List.of(arguments("1, 2, 3", new int[] { 1, 2, 3 }));
80+
81+
@ParameterizedTest
82+
@FieldSource
83+
void array_of_primitive_longs(@Elements long[] elements, long[] expected) {
84+
assertThat(elements).isEqualTo(expected);
85+
}
86+
87+
static List<?> array_of_primitive_longs = List.of(arguments("1, 2, 3", new long[] { 1L, 2L, 3L }));
88+
89+
@ParameterizedTest
90+
@FieldSource
91+
void array_of_primitive_floats(@Elements float[] elements, float[] expected) {
92+
assertThat(elements).isEqualTo(expected);
93+
}
94+
95+
static List<?> array_of_primitive_floats = List.of(arguments("1, 2, 3", new float[] { 1.0f, 2.0f, 3.0f }));
96+
97+
@ParameterizedTest
98+
@FieldSource
99+
void array_of_primitive_doubles(@Elements double[] elements, double[] expected) {
100+
assertThat(elements).isEqualTo(expected);
101+
}
102+
103+
static List<?> array_of_primitive_doubles = List.of(arguments("1, 2, 3", new double[] { 1.0, 2.0, 3.0 }));
104+
105+
@ParameterizedTest
106+
@FieldSource
107+
void array_of_integers(@Elements Integer[] elements, Integer[] expected) {
108+
assertThat(elements).isEqualTo(expected);
109+
}
110+
111+
static List<?> array_of_integers = List.of(arguments("1, 2, 3", new Integer[] { 1, 2, 3 }));
112+
113+
@ParameterizedTest
114+
@FieldSource
115+
void array_of_strings(@Elements String[] elements, String[] expected) {
116+
assertThat(elements).isEqualTo(expected);
117+
}
118+
119+
static List<?> array_of_strings = List.of(arguments("a, b, c", new String[] { "a", "b", "c" }));
120+
121+
@ParameterizedTest
122+
@FieldSource
123+
void iterable_of_integers(@Elements Iterable<Integer> elements, Iterable<Integer> expected) {
124+
assertThat(elements).isEqualTo(expected);
125+
}
126+
127+
static List<?> iterable_of_integers = List.of(arguments("1, 2, 3", List.of(1, 2, 3)));
128+
129+
@ParameterizedTest
130+
@FieldSource
131+
void collection_of_integers(@Elements Collection<Integer> elements, Collection<Integer> expected) {
132+
assertThat(elements).isEqualTo(expected);
133+
}
134+
135+
static List<?> collection_of_integers = List.of(arguments("1, 2, 3", List.of(1, 2, 3)));
136+
137+
@ParameterizedTest
138+
@FieldSource
139+
void list_of_integers(@Elements List<Integer> elements, List<Integer> expected) {
140+
assertThat(elements).isEqualTo(expected);
141+
}
142+
143+
static List<?> list_of_integers = List.of(arguments("1, 2, 3", List.of(1, 2, 3)));
144+
145+
@ParameterizedTest
146+
@FieldSource
147+
void set_of_integers(@Elements Set<Integer> elements, Set<Integer> expected) {
148+
assertThat(elements).isEqualTo(expected);
149+
}
150+
151+
static List<?> set_of_integers = List.of(arguments("1, 2, 3", Set.of(1, 2, 3)));
152+
153+
@ParameterizedTest
154+
@FieldSource
155+
void list_of_strings(@Elements List<String> elements, List<String> expected) {
156+
assertThat(elements).isEqualTo(expected);
157+
}
158+
159+
static List<?> list_of_strings = List.of( //
160+
arguments("a, b, c", List.of("a", "b", "c")), //
161+
arguments(" a, b ,c ", List.of("a", "b", "c")));
162+
163+
}
164+
165+
}

0 commit comments

Comments
 (0)