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")))
|
.notes(blankToNull(text(node, "notes")))
|
||||||
.birthYear(intOrNull(node, "birthYear"))
|
.birthYear(intOrNull(node, "birthYear"))
|
||||||
.deathYear(intOrNull(node, "deathYear"))
|
.deathYear(intOrNull(node, "deathYear"))
|
||||||
|
.generation(generationOrNull(node, personId))
|
||||||
.familyMember(node.path("familyMember").asBoolean(false))
|
.familyMember(node.path("familyMember").asBoolean(false))
|
||||||
.personType(PersonType.PERSON)
|
.personType(PersonType.PERSON)
|
||||||
.provisional(false)
|
.provisional(false)
|
||||||
.build();
|
.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) {
|
private int createRelationships(JsonNode relationships, Map<String, UUID> idByRowId) {
|
||||||
int created = 0;
|
int created = 0;
|
||||||
for (JsonNode node : relationships) {
|
for (JsonNode node : relationships) {
|
||||||
|
|||||||
@@ -151,6 +151,65 @@ class PersonTreeImporterTest {
|
|||||||
verify(relationshipService, org.mockito.Mockito.never()).addRelationship(any(), any());
|
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) {
|
private static Person personOf(PersonUpsertCommand cmd) {
|
||||||
return Person.builder().id(UUID.randomUUID()).sourceRef(cmd.sourceRef()).lastName(cmd.lastName()).build();
|
return Person.builder().id(UUID.randomUUID()).sourceRef(cmd.sourceRef()).lastName(cmd.lastName()).build();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user