diff --git a/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/DecodedFact.java b/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/DecodedFact.java new file mode 100644 index 00000000..f155d039 --- /dev/null +++ b/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/DecodedFact.java @@ -0,0 +1,215 @@ +package com.lingniu.ingest.facts; + +import com.lingniu.ingest.api.ProtocolId; + +import java.time.Instant; +import java.util.Map; + +public sealed interface DecodedFact + permits DecodedFact.Location, + DecodedFact.Realtime, + DecodedFact.Alarm, + DecodedFact.Session, + DecodedFact.Register, + DecodedFact.Extension { + + String factId(); + String frameId(); + ProtocolId protocol(); + String vehicleKey(); + String vin(); + String phone(); + Instant eventTime(); + Instant receivedAt(); + String rawUri(); + Map metadata(); + + record Location( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + Double longitude, + Double latitude, + Double altitudeM, + Double speedKmh, + Double directionDeg, + Long alarmFlag, + Long statusFlag, + Double totalMileageKm + ) implements DecodedFact { + public Location { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + metadata = BaseFields.metadata(metadata); + } + } + + record Realtime( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + Map fields + ) implements DecodedFact { + public Realtime { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + metadata = BaseFields.metadata(metadata); + fields = fields == null ? Map.of() : Map.copyOf(fields); + } + } + + record Alarm( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + String alarmLevel, + Long alarmCode, + String alarmName, + Map fields + ) implements DecodedFact { + public Alarm { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + alarmLevel = BaseFields.clean(alarmLevel); + alarmName = BaseFields.clean(alarmName); + metadata = BaseFields.metadata(metadata); + fields = fields == null ? Map.of() : Map.copyOf(fields); + } + } + + record Session( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + String sessionType, + Integer resultCode, + Map fields + ) implements DecodedFact { + public Session { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + sessionType = BaseFields.clean(sessionType); + metadata = BaseFields.metadata(metadata); + fields = fields == null ? Map.of() : Map.copyOf(fields); + } + } + + record Register( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + Map registrationFields + ) implements DecodedFact { + public Register { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + metadata = BaseFields.metadata(metadata); + registrationFields = registrationFields == null ? Map.of() : Map.copyOf(registrationFields); + } + } + + record Extension( + String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + String vin, + String phone, + Instant eventTime, + Instant receivedAt, + String rawUri, + Map metadata, + String extensionType, + Map fields + ) implements DecodedFact { + public Extension { + BaseFields.validate(factId, frameId, protocol, vehicleKey, eventTime, receivedAt, rawUri); + vin = BaseFields.clean(vin); + phone = BaseFields.clean(phone); + extensionType = BaseFields.clean(extensionType); + metadata = BaseFields.metadata(metadata); + fields = fields == null ? Map.of() : Map.copyOf(fields); + } + } + + final class BaseFields { + private BaseFields() { + } + + static void validate(String factId, + String frameId, + ProtocolId protocol, + String vehicleKey, + Instant eventTime, + Instant receivedAt, + String rawUri) { + required(factId, "factId"); + required(frameId, "frameId"); + if (protocol == null) { + throw new IllegalArgumentException("protocol must not be null"); + } + required(vehicleKey, "vehicleKey"); + if (eventTime == null) { + throw new IllegalArgumentException("eventTime must not be null"); + } + if (receivedAt == null) { + throw new IllegalArgumentException("receivedAt must not be null"); + } + required(rawUri, "rawUri"); + } + + static String clean(String value) { + return value == null ? "" : value.trim(); + } + + static Map metadata(Map metadata) { + return metadata == null ? Map.of() : Map.copyOf(metadata); + } + + private static void required(String value, String field) { + if (clean(value).isBlank()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + } + } +} diff --git a/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/DecodedFactTest.java b/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/DecodedFactTest.java new file mode 100644 index 00000000..06f71891 --- /dev/null +++ b/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/DecodedFactTest.java @@ -0,0 +1,39 @@ +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; + +class DecodedFactTest { + + @Test + void locationFactCarriesRawReferenceAndMetadata() { + DecodedFact.Location location = new DecodedFact.Location( + "fact-1", + "frame-1", + ProtocolId.JT808, + "jt808:013912345678", + "", + "013912345678", + Instant.parse("2026-06-29T00:00:00Z"), + Instant.parse("2026-06-29T00:00:01Z"), + "archive://jt808/frame-1.bin", + Map.of("messageId", "0x0200"), + 120.123456, + 30.123456, + 10.0, + 42.0, + 180.0, + 0L, + 2L, + 1000.5); + + assertThat(location.factId()).isEqualTo("fact-1"); + assertThat(location.rawUri()).isEqualTo("archive://jt808/frame-1.bin"); + assertThat(location.metadata()).containsEntry("messageId", "0x0200"); + } +}