feat: add raw frame fact model
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.lingniu.ingest.facts;
|
||||||
|
|
||||||
|
public enum ParseStatus {
|
||||||
|
NOT_PARSED,
|
||||||
|
SUCCEEDED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user