Precompute matched-transition link keys for HTML export

Embed linkKey on matchedTransitions in HTML metadata so explorer highlighting uses the same SVG #link_* suffix as PlantUML, avoiding JS/Java format drift while keeping golden JSON unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 06:46:23 +02:00
parent 32fe96041c
commit 00d1f24709
6 changed files with 142 additions and 5 deletions

View File

@@ -1,16 +1,20 @@
package click.kamil.springstatemachineexporter.analysis.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import lombok.extern.jackson.Jacksonized;
@Data
@Builder
@Builder(toBuilder = true)
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class MatchedTransition {
private final String sourceState;
private final String targetState;
private final String event;
/** Precomputed {@code #link_*} suffix for HTML/SVG highlight; optional in JSON exports. */
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String linkKey;
}

View File

@@ -0,0 +1,60 @@
package click.kamil.springstatemachineexporter.exporter;
/**
* Builds SVG/HTML transition link keys ({@code Source__Event}) using the same formatting rules as
* {@link PlantUml} embedded identifiers and the HTML explorer {@code buildLinkKey} helper.
*/
public final class TransitionLinkKey {
private TransitionLinkKey() {
}
public static String build(String sourceStateIdentifier, String eventIdentifier) {
return build(sourceStateIdentifier, eventIdentifier, EnumFormat.fn, EnumFormat.fn);
}
public static String build(
String sourceStateIdentifier,
String eventIdentifier,
EnumFormat stateFormat,
EnumFormat eventFormat) {
if (eventIdentifier == null || eventIdentifier.isBlank()) {
return "";
}
String formattedEvent = formatIdentifier(eventIdentifier, eventFormat);
if (sourceStateIdentifier == null || sourceStateIdentifier.isBlank()) {
return "__" + normalize(formattedEvent);
}
return normalize(formatIdentifier(sourceStateIdentifier, stateFormat))
+ "__"
+ normalize(formattedEvent);
}
static String formatIdentifier(String identifier, EnumFormat format) {
if (identifier == null || identifier.isBlank()) {
return "";
}
return switch (format) {
case fqn -> identifier;
case sn -> {
int lastDot = identifier.lastIndexOf('.');
yield lastDot >= 0 ? identifier.substring(lastDot + 1) : identifier;
}
case fn -> {
int lastDot = identifier.lastIndexOf('.');
if (lastDot <= 0) {
yield identifier;
}
int prevDot = identifier.lastIndexOf('.', lastDot - 1);
yield prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
}
};
}
static String normalize(String value) {
if (value == null || value.isBlank()) {
return "";
}
return value.replaceAll("[^a-zA-Z0-9]", "_");
}
}

View File

@@ -0,0 +1,32 @@
package click.kamil.springstatemachineexporter.exporter;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TransitionLinkKeyTest {
@Test
void shouldBuildFnFormLinkKeyFromPackageCanonicalIdentifiers() {
assertThat(TransitionLinkKey.build(
"com.example.order.OrderState.NEW",
"com.example.order.OrderEvent.PAY"))
.isEqualTo("OrderState_NEW__OrderEvent_PAY");
}
@Test
void shouldBuildFqnFormLinkKeyWhenRequested() {
assertThat(TransitionLinkKey.build(
"com.example.order.OrderState.NEW",
"com.example.order.OrderEvent.PAY",
EnumFormat.fqn,
EnumFormat.fqn))
.isEqualTo("com_example_order_OrderState_NEW__com_example_order_OrderEvent_PAY");
}
@Test
void shouldBuildEventOnlyLinkKeyWhenSourceMissing() {
assertThat(TransitionLinkKey.build(null, "com.example.order.OrderEvent.PAY"))
.isEqualTo("__OrderEvent_PAY");
}
}