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