feat(import): pass generation from JSON in PersonTreeImporter (#689)
Reads the optional `generation` integer from the canonical tree JSON and routes it into PersonUpsertCommand. Out-of-range values are skip-and- warned with the same policy as the register importer. Tree imports run after register (per CanonicalImportOrchestrator); a tree-confirmed integer overwrites a register-parsed value — both sides are "canonical" in preferHuman terms (neither is a human edit). Refs #689 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -79,12 +79,32 @@ public class PersonTreeImporter {
|
||||
.notes(blankToNull(text(node, "notes")))
|
||||
.birthYear(intOrNull(node, "birthYear"))
|
||||
.deathYear(intOrNull(node, "deathYear"))
|
||||
.generation(generationOrNull(node, personId))
|
||||
.familyMember(node.path("familyMember").asBoolean(false))
|
||||
.personType(PersonType.PERSON)
|
||||
.provisional(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON {@code generation} value if present and in
|
||||
* {@value #GENERATION_MIN}..{@value #GENERATION_MAX}; null otherwise. Out-of-range
|
||||
* values log a WARN but never abort the batch — mirrors the register-importer
|
||||
* skip-and-warn policy.
|
||||
*/
|
||||
private static Integer generationOrNull(JsonNode node, String personId) {
|
||||
Integer raw = intOrNull(node, "generation");
|
||||
if (raw == null) return null;
|
||||
if (raw < GENERATION_MIN || raw > GENERATION_MAX) {
|
||||
log.warn("Skipping out-of-range generation '{}' for person {}", raw, personId);
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static final int GENERATION_MIN = 0;
|
||||
private static final int GENERATION_MAX = 10;
|
||||
|
||||
private int createRelationships(JsonNode relationships, Map<String, UUID> idByRowId) {
|
||||
int created = 0;
|
||||
for (JsonNode node : relationships) {
|
||||
|
||||
@@ -151,6 +151,65 @@ class PersonTreeImporterTest {
|
||||
verify(relationshipService, org.mockito.Mockito.never()).addRelationship(any(), any());
|
||||
}
|
||||
|
||||
// ─── generation (#689) ─────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void load_passesGenerationFromJson(@TempDir Path tempDir) throws Exception {
|
||||
PersonService personService = mock(PersonService.class);
|
||||
RelationshipService relationshipService = mock(RelationshipService.class);
|
||||
when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0)));
|
||||
Path json = write(tempDir, """
|
||||
{"persons":[
|
||||
{"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true,
|
||||
"personId":"herbert-cram","generation":3}
|
||||
],"relationships":[]}
|
||||
""");
|
||||
|
||||
new PersonTreeImporter(personService, relationshipService).load(json.toFile());
|
||||
|
||||
ArgumentCaptor<PersonUpsertCommand> captor = ArgumentCaptor.forClass(PersonUpsertCommand.class);
|
||||
verify(personService).upsertBySourceRef(captor.capture());
|
||||
assertThat(captor.getValue().generation()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void load_returnsNullGeneration_whenAbsentFromJson(@TempDir Path tempDir) throws Exception {
|
||||
PersonService personService = mock(PersonService.class);
|
||||
RelationshipService relationshipService = mock(RelationshipService.class);
|
||||
when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0)));
|
||||
Path json = write(tempDir, """
|
||||
{"persons":[
|
||||
{"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true,
|
||||
"personId":"herbert-cram"}
|
||||
],"relationships":[]}
|
||||
""");
|
||||
|
||||
new PersonTreeImporter(personService, relationshipService).load(json.toFile());
|
||||
|
||||
ArgumentCaptor<PersonUpsertCommand> captor = ArgumentCaptor.forClass(PersonUpsertCommand.class);
|
||||
verify(personService).upsertBySourceRef(captor.capture());
|
||||
assertThat(captor.getValue().generation()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void load_skipsOutOfRangeGeneration_logsWarn_neverAborts(@TempDir Path tempDir) throws Exception {
|
||||
PersonService personService = mock(PersonService.class);
|
||||
RelationshipService relationshipService = mock(RelationshipService.class);
|
||||
when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0)));
|
||||
Path json = write(tempDir, """
|
||||
{"persons":[
|
||||
{"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true,
|
||||
"personId":"herbert-cram","generation":99}
|
||||
],"relationships":[]}
|
||||
""");
|
||||
|
||||
new PersonTreeImporter(personService, relationshipService).load(json.toFile());
|
||||
|
||||
ArgumentCaptor<PersonUpsertCommand> captor = ArgumentCaptor.forClass(PersonUpsertCommand.class);
|
||||
verify(personService).upsertBySourceRef(captor.capture());
|
||||
assertThat(captor.getValue().generation()).isNull();
|
||||
}
|
||||
|
||||
private static Person personOf(PersonUpsertCommand cmd) {
|
||||
return Person.builder().id(UUID.randomUUID()).sourceRef(cmd.sourceRef()).lastName(cmd.lastName()).build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user