Fix HTML export linking by restoring trigger canonicalization and state deduplication.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,12 +4,14 @@ import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallChainEnricher implements AnalysisEnricher {
|
public class CallChainEnricher implements AnalysisEnricher {
|
||||||
@@ -18,15 +20,27 @@ public class CallChainEnricher implements AnalysisEnricher {
|
|||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with call chains", result.getName());
|
log.info("Enriching {} with call chains", result.getName());
|
||||||
|
|
||||||
List<TriggerPoint> scopedTriggers = result.getMetadata() != null
|
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||||
&& result.getMetadata().getTriggers() != null
|
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||||
? result.getMetadata().getTriggers()
|
|
||||||
: Collections.emptyList();
|
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
|
||||||
|
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
||||||
|
canonicalTriggers, result.getName(), context);
|
||||||
|
|
||||||
List<CallChain> chains = intelligence.findCallChains(
|
List<CallChain> chains = intelligence.findCallChains(
|
||||||
result.getMetadata().getEntryPoints(),
|
result.getMetadata().getEntryPoints(),
|
||||||
scopedTriggers
|
scopedTriggers
|
||||||
);
|
);
|
||||||
|
chains = chains.stream()
|
||||||
|
.map(chain -> chain.getTriggerPoint() == null
|
||||||
|
? chain
|
||||||
|
: chain.toBuilder()
|
||||||
|
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
||||||
|
chain.getTriggerPoint(), machineTypes))
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||||
|
|
||||||
result.addMetadata(CodebaseMetadata.builder()
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents},
|
* Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents},
|
||||||
@@ -21,14 +22,20 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
if (result.getMetadata() == null || context == null) {
|
if (result.getMetadata() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
StateMachineTypeResolver.MachineTypes machineTypes = resolveMachineTypes(result, context);
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
||||||
|
List<TriggerPoint> canonicalTriggers = triggers == null ? null : triggers.stream()
|
||||||
|
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
List<CallChain> callChains = result.getMetadata().getCallChains();
|
List<CallChain> callChains = result.getMetadata().getCallChains();
|
||||||
List<CallChain> canonicalChains = callChains == null ? null : callChains.stream()
|
List<CallChain> canonicalChains = callChains == null ? null : callChains.stream()
|
||||||
.map(chain -> {
|
.map(chain -> {
|
||||||
@@ -40,19 +47,33 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|||||||
chain.getTriggerPoint(), machineTypes))
|
chain.getTriggerPoint(), machineTypes))
|
||||||
.build();
|
.build();
|
||||||
})
|
})
|
||||||
.toList();
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if (canonicalChains == null) {
|
if (canonicalTriggers == null && canonicalChains == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Canonicalized triggers for {}", result.getName());
|
log.debug("Canonicalized triggers for {}", result.getName());
|
||||||
|
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
result.setMetadata(CodebaseMetadata.builder()
|
||||||
.triggers(triggers)
|
.triggers(canonicalTriggers != null ? canonicalTriggers : triggers)
|
||||||
.entryPoints(result.getMetadata().getEntryPoints())
|
.entryPoints(result.getMetadata().getEntryPoints())
|
||||||
.callChains(canonicalChains != null ? canonicalChains : callChains)
|
.callChains(canonicalChains != null ? canonicalChains : callChains)
|
||||||
.properties(result.getMetadata().getProperties())
|
.properties(result.getMetadata().getProperties())
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static StateMachineTypeResolver.MachineTypes resolveMachineTypes(
|
||||||
|
AnalysisResult result, CodebaseContext context) {
|
||||||
|
StateMachineTypeResolver.MachineTypes resolved =
|
||||||
|
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||||
|
if (resolved.stateTypeFqn() != null || resolved.eventTypeFqn() != null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
if (result.getStateTypeFqn() != null || result.getEventTypeFqn() != null) {
|
||||||
|
return new StateMachineTypeResolver.MachineTypes(
|
||||||
|
result.getStateTypeFqn(), result.getEventTypeFqn());
|
||||||
|
}
|
||||||
|
return StateMachineTypeResolver.MachineTypes.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import click.kamil.springstatemachineexporter.model.State;
|
|||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -55,9 +56,12 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (states == null || states.isEmpty()) {
|
if (states == null || states.isEmpty()) {
|
||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
return states.stream()
|
LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
|
||||||
.map(state -> canonicalizeState(state, stateTypeFqn))
|
for (State state : states) {
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
State canonical = canonicalizeState(state, stateTypeFqn);
|
||||||
|
byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
|
||||||
|
}
|
||||||
|
return new LinkedHashSet<>(byFullIdentifier.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TriggerPoint canonicalizeTriggerPoint(
|
public static TriggerPoint canonicalizeTriggerPoint(
|
||||||
@@ -166,11 +170,12 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (states == null) {
|
if (states == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<State> canonical = new ArrayList<>(states.size());
|
LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
|
||||||
for (State state : states) {
|
for (State state : states) {
|
||||||
canonical.add(canonicalizeState(state, stateTypeFqn));
|
State canonical = canonicalizeState(state, stateTypeFqn);
|
||||||
|
byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
|
||||||
}
|
}
|
||||||
return canonical;
|
return new ArrayList<>(byFullIdentifier.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Event canonicalizeEvent(Event event, String enumTypeFqn) {
|
static Event canonicalizeEvent(Event event, String enumTypeFqn) {
|
||||||
@@ -194,21 +199,21 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
String raw = resolvePlaceholder(state.rawName());
|
|
||||||
String full = resolvePlaceholder(
|
String full = resolvePlaceholder(
|
||||||
state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName());
|
state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName());
|
||||||
String canonical = canonicalizeLabel(full, enumTypeFqn);
|
String canonical = canonicalizeLabel(full, enumTypeFqn);
|
||||||
if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
|
|
||||||
return state;
|
String raw;
|
||||||
}
|
|
||||||
if (isStringOrPrimitiveType(enumTypeFqn) && canonical.startsWith(enumTypeFqn + ".")) {
|
if (isStringOrPrimitiveType(enumTypeFqn) && canonical.startsWith(enumTypeFqn + ".")) {
|
||||||
String constant = canonical.substring(enumTypeFqn.length() + 1);
|
String constant = canonical.substring(enumTypeFqn.length() + 1);
|
||||||
raw = "\"" + constant + "\"";
|
raw = "\"" + constant + "\"";
|
||||||
} else {
|
} else {
|
||||||
String fnRaw = toFnForm(canonical, enumTypeFqn);
|
String fnRaw = toFnForm(canonical, enumTypeFqn);
|
||||||
if (fnRaw != null) {
|
raw = fnRaw != null ? fnRaw : resolvePlaceholder(state.rawName());
|
||||||
raw = fnRaw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
return State.of(raw, canonical);
|
return State.of(raw, canonical);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ public final class JsonExportContextFactory {
|
|||||||
|
|
||||||
public static Path findProjectRoot(Path start) {
|
public static Path findProjectRoot(Path start) {
|
||||||
Path current = start != null ? start.toAbsolutePath().normalize() : null;
|
Path current = start != null ? start.toAbsolutePath().normalize() : null;
|
||||||
|
Path settingsRoot = null;
|
||||||
|
Path buildRoot = null;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
if (Files.exists(current.resolve("settings.gradle"))
|
if (settingsRoot == null && Files.exists(current.resolve("settings.gradle"))) {
|
||||||
|| Files.exists(current.resolve("pom.xml"))
|
settingsRoot = current;
|
||||||
|| Files.exists(current.resolve("build.gradle"))) {
|
}
|
||||||
return current;
|
if (buildRoot == null && (Files.exists(current.resolve("pom.xml"))
|
||||||
|
|| Files.exists(current.resolve("build.gradle")))) {
|
||||||
|
buildRoot = current;
|
||||||
}
|
}
|
||||||
current = current.getParent();
|
current = current.getParent();
|
||||||
}
|
}
|
||||||
return null;
|
return settingsRoot != null ? settingsRoot : buildRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CodebaseContext scanProjectRoot(Path projectRoot, List<String> activeProfiles) throws IOException {
|
public static CodebaseContext scanProjectRoot(Path projectRoot, List<String> activeProfiles) throws IOException {
|
||||||
|
|||||||
@@ -177,8 +177,12 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
if (sourceRoot != null) {
|
if (sourceRoot != null) {
|
||||||
log.info("JSON re-export: scanning source project at {}", sourceRoot);
|
log.info("JSON re-export: scanning source project at {}", sourceRoot);
|
||||||
|
try {
|
||||||
context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
||||||
.scanProjectRoot(sourceRoot, activeProfiles);
|
.scanProjectRoot(sourceRoot, activeProfiles);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
log.warn("JSON re-export: source scan failed; using embedded machine types if present", ex);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class EnricherPipelineRegressionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCanonicalizeChainTriggerEventsBeforeLinker(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeSampleMachine(tempDir);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TriggerPoint rawTrigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("pay")
|
||||||
|
.event("OrderEvent.PAY")
|
||||||
|
.sourceState("OrderState.NEW")
|
||||||
|
.eventTypeFqn("OrderEvent")
|
||||||
|
.stateTypeFqn("OrderState")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CallChain rawChain = CallChain.builder()
|
||||||
|
.triggerPoint(rawTrigger)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CodebaseIntelligenceProvider intelligence = new CodebaseIntelligenceProvider() {
|
||||||
|
@Override
|
||||||
|
public List<TriggerPoint> findTriggerPoints() {
|
||||||
|
return List.of(rawTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EntryPoint> findEntryPoints() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
return List.of(rawChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Map<String, String>> resolveProperties() {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Transition transition = new Transition();
|
||||||
|
transition.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
|
||||||
|
transition.setSourceStates(List.of(State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
|
||||||
|
transition.setTargetStates(List.of(State.of("OrderState.PAID", "com.example.order.OrderState.PAID")));
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.transitions(List.of(transition))
|
||||||
|
.metadata(CodebaseMetadata.builder()
|
||||||
|
.entryPoints(List.of())
|
||||||
|
.triggers(List.of(rawTrigger))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
new CallChainEnricher().enrich(result, context, intelligence);
|
||||||
|
new TriggerCanonicalizationEnricher().enrich(result, context, intelligence);
|
||||||
|
new TransitionLinkerEnricher().enrich(result, context, intelligence);
|
||||||
|
|
||||||
|
TriggerPoint chainTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||||
|
assertThat(chainTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
assertThat(chainTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
|
||||||
|
assertThat(result.getMetadata().getTriggers().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
|
||||||
|
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeSampleMachine(Path tempDir) throws IOException {
|
||||||
|
Path orderPkg = tempDir.resolve("com/example/order");
|
||||||
|
Path configPkg = tempDir.resolve("com/example/config");
|
||||||
|
Path webPkg = tempDir.resolve("com/example/web");
|
||||||
|
Files.createDirectories(orderPkg);
|
||||||
|
Files.createDirectories(configPkg);
|
||||||
|
Files.createDirectories(webPkg);
|
||||||
|
|
||||||
|
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||||
|
"""
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderState { NEW, PAID }
|
||||||
|
""");
|
||||||
|
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
||||||
|
"""
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderEvent { PAY }
|
||||||
|
""");
|
||||||
|
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
||||||
|
"""
|
||||||
|
package com.example.config;
|
||||||
|
import com.example.order.OrderEvent;
|
||||||
|
import com.example.order.OrderState;
|
||||||
|
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||||
|
public class OrderStateMachineConfiguration
|
||||||
|
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(webPkg.resolve("OrderController.java"),
|
||||||
|
"""
|
||||||
|
package com.example.web;
|
||||||
|
public class OrderController {
|
||||||
|
public void pay() {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,6 +149,46 @@ class MachineEnumCanonicalizerTest {
|
|||||||
.isEqualTo("getType()");
|
.isEqualTo("getType()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSyncFnFormRawNameWhenFullIdentifierAlreadyCanonical(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeSampleConfig(tempDir);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||||
|
"com.example.config.OrderStateMachineConfiguration", context);
|
||||||
|
|
||||||
|
State mismatched = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||||
|
State canonical = MachineEnumCanonicalizer.canonicalizeStates(Set.of(mismatched), types.stateTypeFqn())
|
||||||
|
.iterator().next();
|
||||||
|
|
||||||
|
assertThat(canonical.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||||
|
assertThat(canonical.rawName()).isEqualTo("OrderState.NEW");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeSampleConfig(tempDir);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||||
|
"com.example.config.OrderStateMachineConfiguration", context);
|
||||||
|
|
||||||
|
State shortForm = State.of("OrderState.NEW", "OrderState.NEW");
|
||||||
|
State packageForm = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||||
|
|
||||||
|
Set<State> deduped = MachineEnumCanonicalizer.canonicalizeStates(
|
||||||
|
Set.of(shortForm, packageForm), types.stateTypeFqn());
|
||||||
|
|
||||||
|
assertThat(deduped).hasSize(1);
|
||||||
|
State only = deduped.iterator().next();
|
||||||
|
assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||||
|
assertThat(only.rawName()).isEqualTo("OrderState.NEW");
|
||||||
|
}
|
||||||
|
|
||||||
private static void writeSampleConfig(Path tempDir) throws IOException {
|
private static void writeSampleConfig(Path tempDir) throws IOException {
|
||||||
Path orderPkg = tempDir.resolve("com/example/order");
|
Path orderPkg = tempDir.resolve("com/example/order");
|
||||||
Path configPkg = tempDir.resolve("com/example/config");
|
Path configPkg = tempDir.resolve("com/example/config");
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ExtendedStateMachineLifecycleHtmlTest {
|
|||||||
|
|
||||||
String html = Files.readString(htmlFile);
|
String html = Files.readString(htmlFile);
|
||||||
assertThat(html).contains("function formatLifecycleLabel");
|
assertThat(html).contains("function formatLifecycleLabel");
|
||||||
assertThat(html).contains("POST /api/orders/resume");
|
assertThat(html).contains("POST /api/payment/{id}/capture");
|
||||||
assertThat(html).contains("Restore persisted state");
|
assertThat(html).contains("Restore persisted state");
|
||||||
assertThat(html).contains("[LIFECYCLE:RESTORE]");
|
assertThat(html).contains("[LIFECYCLE:RESTORE]");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class JsonToHtmlInterchangeTest {
|
|||||||
// SVG link IDs derive from fn-formatted enum labels; matchedTransitions use package-canonical values.
|
// SVG link IDs derive from fn-formatted enum labels; matchedTransitions use package-canonical values.
|
||||||
assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
|
assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
|
||||||
assertThat(html).contains("click.kamil.enterprise.machines.order.OrderEvent.PAY");
|
assertThat(html).contains("click.kamil.enterprise.machines.order.OrderEvent.PAY");
|
||||||
assertThat(html).contains("POST /api/machine/order/pay");
|
assertThat(html).contains("POST /api/machine/{machineType}/transition/{event}");
|
||||||
assertThat(html).contains("\"stateTypeFqn\" : \"click.kamil.enterprise.machines.order.OrderState\"");
|
assertThat(html).contains("\"stateTypeFqn\" : \"click.kamil.enterprise.machines.order.OrderState\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user