feat: add raw frame fact model

This commit is contained in:
lingniu
2026-06-29 12:54:16 +08:00
parent aed023043c
commit b5453ff888
5 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
package com.lingniu.ingest.facts;
import com.lingniu.ingest.api.ProtocolId;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;
public final class FactIds {
private FactIds() {
}
public static String rawFrameId(ProtocolId protocol,
String vehicleKey,
int messageId,
int subType,
Instant receivedAt,
String checksum) {
if (protocol == null) {
throw new IllegalArgumentException("protocol must not be null");
}
String input = protocol.name() + "|"
+ clean(vehicleKey) + "|"
+ messageId + "|"
+ subType + "|"
+ (receivedAt == null ? "" : receivedAt.toString()) + "|"
+ clean(checksum);
return "rf_" + sha256(input).substring(0, 32);
}
public static String decodedFactId(String frameId, String kind, int ordinal) {
return "df_" + clean(frameId) + "_" + clean(kind) + "_" + ordinal;
}
private static String clean(String value) {
return value == null ? "" : value.trim();
}
private static String sha256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is unavailable", e);
}
}
}

View File

@@ -0,0 +1,7 @@
package com.lingniu.ingest.facts;
public enum ParseStatus {
NOT_PARSED,
SUCCEEDED,
FAILED
}

View File

@@ -0,0 +1,65 @@
package com.lingniu.ingest.facts;
import com.lingniu.ingest.api.ProtocolId;
import java.time.Instant;
import java.util.Map;
public record RawFrameFact(
String frameId,
ProtocolId protocol,
String vehicleKey,
String vin,
String phone,
int messageId,
int subType,
Instant eventTime,
Instant receivedAt,
String peer,
String rawUri,
String checksum,
long rawSizeBytes,
ParseStatus parseStatus,
String parseError,
Map<String, String> metadata
) {
public RawFrameFact {
frameId = required(frameId, "frameId");
if (protocol == null) {
throw new IllegalArgumentException("protocol must not be null");
}
vehicleKey = required(vehicleKey, "vehicleKey");
receivedAt = required(receivedAt, "receivedAt");
eventTime = eventTime == null ? receivedAt : eventTime;
rawUri = required(rawUri, "rawUri");
checksum = required(checksum, "checksum");
if (rawSizeBytes < 0) {
throw new IllegalArgumentException("rawSizeBytes must not be negative");
}
parseStatus = parseStatus == null ? ParseStatus.NOT_PARSED : parseStatus;
vin = clean(vin);
phone = clean(phone);
peer = clean(peer);
parseError = clean(parseError);
metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
}
private static String required(String value, String field) {
String cleaned = clean(value);
if (cleaned.isBlank()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return cleaned;
}
private static Instant required(Instant value, String field) {
if (value == null) {
throw new IllegalArgumentException(field + " must not be null");
}
return value;
}
private static String clean(String value) {
return value == null ? "" : value.trim();
}
}

View File

@@ -0,0 +1,38 @@
package com.lingniu.ingest.facts;
import com.lingniu.ingest.api.ProtocolId;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
class FactIdsTest {
@Test
void rawFrameIdIsStableForSameInputs() {
String first = FactIds.rawFrameId(
ProtocolId.GB32960,
"VIN001",
2,
7,
Instant.parse("2026-06-29T00:00:00Z"),
"sha256:abc");
String second = FactIds.rawFrameId(
ProtocolId.GB32960,
"VIN001",
2,
7,
Instant.parse("2026-06-29T00:00:00Z"),
"sha256:abc");
assertThat(first).isEqualTo(second).startsWith("rf_");
}
@Test
void decodedFactIdIncludesKind() {
String id = FactIds.decodedFactId("rf_123", "location", 0);
assertThat(id).isEqualTo("df_rf_123_location_0");
}
}

View File

@@ -0,0 +1,63 @@
package com.lingniu.ingest.facts;
import com.lingniu.ingest.api.ProtocolId;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class RawFrameFactTest {
@Test
void normalizesOptionalStringsAndMetadata() {
RawFrameFact fact = new RawFrameFact(
"frame-1",
ProtocolId.JT808,
"jt808:013912345678",
null,
"013912345678",
0x0200,
0,
null,
Instant.parse("2026-06-29T00:00:00Z"),
null,
"archive://2026/06/29/JT808/jt808-013912345678/frame-1.bin",
"sha256:abc",
128,
ParseStatus.SUCCEEDED,
null,
Map.of("messageName", "LOCATION"));
assertThat(fact.vin()).isEmpty();
assertThat(fact.eventTime()).isEqualTo(fact.receivedAt());
assertThat(fact.peer()).isEmpty();
assertThat(fact.parseError()).isEmpty();
assertThat(fact.metadata()).containsEntry("messageName", "LOCATION");
}
@Test
void rejectsMissingRequiredFields() {
assertThatThrownBy(() -> new RawFrameFact(
"",
ProtocolId.GB32960,
"VIN001",
"VIN001",
"",
2,
0,
Instant.parse("2026-06-29T00:00:00Z"),
Instant.parse("2026-06-29T00:00:00Z"),
"",
"archive://x",
"sha256:abc",
1,
ParseStatus.SUCCEEDED,
"",
Map.of()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("frameId");
}
}