Skip to content

Commit d80dca4

Browse files
committed
hotfix: key element in list
1 parent d0eea93 commit d80dca4

7 files changed

Lines changed: 122 additions & 3 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=1.6.0
1+
version=1.6.1

src/main/java/fr/traqueur/structura/conversion/ValueConverter.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package fr.traqueur.structura.conversion;
22

3+
import fr.traqueur.structura.annotations.Options;
34
import fr.traqueur.structura.annotations.Polymorphic;
45
import fr.traqueur.structura.api.Loadable;
56
import fr.traqueur.structura.exceptions.StructuraException;
@@ -208,9 +209,16 @@ private void convertPolymorphicMapEntries(Map<String, Object> valueMap, Type ele
208209
*/
209210
private void convertMapEntriesToRecords(Map<String, Object> valueMap, Type elementGenericType, Class<?> elementRawType,
210211
Collection<Object> result, String prefix) {
212+
boolean hasKey = hasKeyComponent(elementRawType);
211213
for (Map.Entry<String, Object> entry : valueMap.entrySet()) {
212214
Object itemValue = entry.getValue();
213-
result.add(convert(itemValue, elementGenericType, elementRawType, prefix));
215+
if (hasKey) {
216+
// Wrap as single-entry map so isSimpleKeyMapping works correctly
217+
Map<String, Object> wrappedEntry = Map.of(entry.getKey(), itemValue);
218+
result.add(convert(wrappedEntry, elementGenericType, elementRawType, prefix));
219+
} else {
220+
result.add(convert(itemValue, elementGenericType, elementRawType, prefix));
221+
}
214222
}
215223
}
216224

@@ -537,4 +545,21 @@ private Map<String, Object> enrichWithDiscriminator(Object value, String discrim
537545

538546
return valueMap;
539547
}
548+
549+
/**
550+
* Checks if a record type has a component marked with @Options(isKey = true).
551+
*
552+
* @param recordType the record type to check
553+
* @return true if the record has a key component
554+
*/
555+
private boolean hasKeyComponent(Class<?> recordType) {
556+
if (!recordType.isRecord()) {
557+
return false;
558+
}
559+
return Arrays.stream(recordType.getRecordComponents())
560+
.anyMatch(component -> {
561+
Options options = component.getAnnotation(Options.class);
562+
return options != null && options.isKey();
563+
});
564+
}
540565
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=1.6.0
1+
version=1.6.1

src/test/java/fr/traqueur/structura/conversion/ValueConverterTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ void setUp() {
4848
public Map<String, String> getStringStringMap() { return null; }
4949
public Map<String, Integer> getStringIntegerMap() { return null; }
5050
public List<TestDatabaseConfig> getDatabaseConfigList() { return null; }
51+
public List<SimpleKeyRecord> getSimpleKeyRecordList() { return null; }
5152

5253
@Nested
5354
@DisplayName("Complex Type Conversions")
@@ -200,6 +201,39 @@ void shouldThrowForUnsupportedCollectionTypes() throws Exception {
200201
);
201202
assertTrue(exception.getMessage().contains("Unsupported collection type"));
202203
}
204+
205+
@Test
206+
@DisplayName("Should convert map of records with @Options(isKey=true) to list")
207+
void shouldConvertMapOfKeyRecordsToList() throws Exception {
208+
// This test case replicates the Currency/EconomiesSettings scenario
209+
// where YAML map keys should become the 'id' field value
210+
Map<String, Object> currenciesMap = Map.of(
211+
"first", Map.of("value-int", 100, "value-double", 10.5),
212+
"second", Map.of("value-int", 200, "value-double", 20.5),
213+
"third", Map.of("value-int", 300, "value-double", 30.5)
214+
);
215+
216+
Type listType = ValueConverterTest.class.getMethod("getSimpleKeyRecordList").getGenericReturnType();
217+
218+
@SuppressWarnings("unchecked")
219+
List<SimpleKeyRecord> result = (List<SimpleKeyRecord>) valueConverter.convert(
220+
currenciesMap, listType, List.class, ""
221+
);
222+
223+
assertCollectionSize(3, result, "key record list");
224+
225+
// Find each record by its id (map key becomes the id field)
226+
SimpleKeyRecord first = result.stream().filter(r -> "first".equals(r.id())).findFirst().orElseThrow();
227+
SimpleKeyRecord second = result.stream().filter(r -> "second".equals(r.id())).findFirst().orElseThrow();
228+
SimpleKeyRecord third = result.stream().filter(r -> "third".equals(r.id())).findFirst().orElseThrow();
229+
230+
assertEquals(100, first.valueInt());
231+
assertEquals(10.5, first.valueDouble());
232+
assertEquals(200, second.valueInt());
233+
assertEquals(20.5, second.valueDouble());
234+
assertEquals(300, third.valueInt());
235+
assertEquals(30.5, third.valueDouble());
236+
}
203237
}
204238

205239
@Nested

src/test/java/fr/traqueur/structura/fixtures/TestFixtures.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ private TestFixtures() {} // Prevent instantiation
136136
debug-mode: true
137137
""";
138138

139+
public static final String KEY_RECORD_LIST_CONFIG = """
140+
items:
141+
first:
142+
value-int: 100
143+
value-double: 10.5
144+
second:
145+
value-int: 200
146+
value-double: 20.5
147+
third:
148+
value-int: 300
149+
value-double: 30.5
150+
""";
151+
139152
// ==================== Inline Fields ====================
140153

141154
public static final String INLINE_CONFIG = """

src/test/java/fr/traqueur/structura/fixtures/TestModels.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public record SimpleKeyRecord(
104104
@DefaultDouble(0.0) double valueDouble
105105
) implements Loadable {}
106106

107+
public record KeyRecordListConfig(
108+
List<SimpleKeyRecord> items
109+
) implements Loadable {}
110+
107111
public record NestedKeyRecord(
108112
String host,
109113
@DefaultInt(8080) int port,

src/test/java/fr/traqueur/structura/integration/IntegrationTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,49 @@ private StructuraProcessor getCurrentProcessor() {
261261
}
262262
}
263263

264+
@Nested
265+
@DisplayName("Key Record List Tests")
266+
class KeyRecordListTest {
267+
268+
@Test
269+
@DisplayName("Should parse list of records with @Options(isKey=true) from YAML file")
270+
void shouldParseListOfKeyRecordsFromYamlFile() throws IOException {
271+
Path configFile = createTempYamlFile(KEY_RECORD_LIST_CONFIG);
272+
273+
try {
274+
KeyRecordListConfig config = Structura.load(configFile, KeyRecordListConfig.class);
275+
276+
assertNotNull(config.items());
277+
assertEquals(3, config.items().size());
278+
279+
// Find each record by its id (YAML map key becomes the id field)
280+
SimpleKeyRecord first = config.items().stream()
281+
.filter(r -> "first".equals(r.id()))
282+
.findFirst()
283+
.orElseThrow(() -> new AssertionError("Record with id 'first' not found"));
284+
285+
SimpleKeyRecord second = config.items().stream()
286+
.filter(r -> "second".equals(r.id()))
287+
.findFirst()
288+
.orElseThrow(() -> new AssertionError("Record with id 'second' not found"));
289+
290+
SimpleKeyRecord third = config.items().stream()
291+
.filter(r -> "third".equals(r.id()))
292+
.findFirst()
293+
.orElseThrow(() -> new AssertionError("Record with id 'third' not found"));
294+
295+
assertEquals(100, first.valueInt());
296+
assertEquals(10.5, first.valueDouble());
297+
assertEquals(200, second.valueInt());
298+
assertEquals(20.5, second.valueDouble());
299+
assertEquals(300, third.valueInt());
300+
assertEquals(30.5, third.valueDouble());
301+
} finally {
302+
deleteTempFile(configFile);
303+
}
304+
}
305+
}
306+
264307
@Nested
265308
@DisplayName("End-to-End Workflow Validation")
266309
class EndToEndWorkflowTest {

0 commit comments

Comments
 (0)