146 lines
5.6 KiB
C#
146 lines
5.6 KiB
C#
using System.Text.Json;
|
|
|
|
/// <summary>
|
|
/// Best-effort deserializers for Kafka payloads written to Parquet.
|
|
/// Tolerates legacy/minimal observation shapes (e.g. test outbox rows using
|
|
/// <c>code</c> instead of <c>observationCode</c>).
|
|
/// </summary>
|
|
public static class DataLakeEventParser
|
|
{
|
|
public static ObservationRow ParseObservationRow(string payload, long offset, int partition)
|
|
{
|
|
var d = JsonDocument.Parse(payload).RootElement;
|
|
var encounterId = GetString(d, "encounterId");
|
|
var observationId = GetString(d, "observationId");
|
|
if (string.IsNullOrEmpty(observationId))
|
|
observationId = $"legacy:{encounterId}:{offset}";
|
|
|
|
return new ObservationRow(
|
|
ObservationId : observationId,
|
|
EncounterId : encounterId,
|
|
PatientId : GetString(d, "patientId"),
|
|
Mrn : GetString(d, "mrn"),
|
|
ObservationCode : GetString(d, "observationCode", "code"),
|
|
Value : GetDouble(d, "value"),
|
|
Unit : GetString(d, "unit"),
|
|
Source : GetString(d, "source"),
|
|
RecordedAt : GetTimestampString(d, "recordedAt"),
|
|
KafkaPartition : partition,
|
|
KafkaOffset : offset);
|
|
}
|
|
|
|
public static AlertRow ParseAlertRow(string payload, long offset, int partition)
|
|
{
|
|
var d = JsonDocument.Parse(payload).RootElement;
|
|
return new AlertRow(
|
|
AlertId : GetString(d, "alertId"),
|
|
EncounterId : GetString(d, "encounterId"),
|
|
PatientId : GetString(d, "patientId"),
|
|
AlertType : GetString(d, "alertType"),
|
|
Severity : GetString(d, "severity"),
|
|
Details : GetString(d, "details"),
|
|
ExplanationJson: GetJsonObjectString(d, "explanation"),
|
|
TriggeredAt : GetTimestampString(d, "triggeredAt"),
|
|
KafkaPartition : partition,
|
|
KafkaOffset : offset);
|
|
}
|
|
|
|
public static EncounterStatusRow ParseEncounterRow(string payload, long offset, int partition)
|
|
{
|
|
var d = JsonDocument.Parse(payload).RootElement;
|
|
return new EncounterStatusRow(
|
|
EncounterId : GetString(d, "encounterId"),
|
|
PatientId : GetString(d, "patientId"),
|
|
PreviousStatus : GetString(d, "previousStatus"),
|
|
NewStatus : GetString(d, "newStatus"),
|
|
ChangedAt : GetTimestampString(d, "changedAt"),
|
|
KafkaPartition : partition,
|
|
KafkaOffset : offset);
|
|
}
|
|
|
|
public static string ExtractDatePath(string topic, string payload, KafkaTopicOptions topics)
|
|
{
|
|
try
|
|
{
|
|
var doc = JsonDocument.Parse(payload);
|
|
var ts = topic switch
|
|
{
|
|
var t when t == topics.ObservationRecorded => GetTimestamp(doc.RootElement, "recordedAt"),
|
|
var t when t == topics.AlertGenerated => GetTimestamp(doc.RootElement, "triggeredAt"),
|
|
var t when t == topics.EncounterStatusChanged => GetTimestamp(doc.RootElement, "changedAt"),
|
|
_ => DateTimeOffset.UtcNow,
|
|
};
|
|
return $"{ts.Year:D4}/{ts.Month:D2}/{ts.Day:D2}";
|
|
}
|
|
catch
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
return $"{now.Year:D4}/{now.Month:D2}/{now.Day:D2}";
|
|
}
|
|
}
|
|
|
|
private static string GetJsonObjectString(JsonElement d, string name)
|
|
{
|
|
if (!TryGetProperty(d, name, out var prop) || prop.ValueKind != JsonValueKind.Object)
|
|
return "";
|
|
return prop.GetRawText();
|
|
}
|
|
|
|
private static string GetString(JsonElement d, string primary, string? alternate = null)
|
|
{
|
|
if (TryGetProperty(d, primary, out var prop))
|
|
return ElementToString(prop);
|
|
if (alternate is not null && TryGetProperty(d, alternate, out prop))
|
|
return ElementToString(prop);
|
|
return "";
|
|
}
|
|
|
|
private static double GetDouble(JsonElement d, string name)
|
|
{
|
|
if (!TryGetProperty(d, name, out var prop))
|
|
return 0;
|
|
|
|
return prop.ValueKind switch
|
|
{
|
|
JsonValueKind.Number => prop.GetDouble(),
|
|
JsonValueKind.String => double.TryParse(prop.GetString(), out var v) ? v : 0,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
private static string GetTimestampString(JsonElement d, string name)
|
|
{
|
|
if (!TryGetProperty(d, name, out var prop))
|
|
return "";
|
|
|
|
if (prop.ValueKind == JsonValueKind.String)
|
|
return prop.GetString() ?? "";
|
|
|
|
return prop.TryGetDateTimeOffset(out var dt) ? dt.ToString("O") : "";
|
|
}
|
|
|
|
private static DateTimeOffset GetTimestamp(JsonElement d, string name)
|
|
{
|
|
if (!TryGetProperty(d, name, out var prop))
|
|
return DateTimeOffset.UtcNow;
|
|
|
|
if (prop.ValueKind == JsonValueKind.String)
|
|
return prop.GetDateTimeOffset();
|
|
|
|
return prop.TryGetDateTimeOffset(out var dt) ? dt : DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
private static bool TryGetProperty(JsonElement d, string name, out JsonElement prop) =>
|
|
d.TryGetProperty(name, out prop) && prop.ValueKind != JsonValueKind.Null;
|
|
|
|
private static string ElementToString(JsonElement prop) =>
|
|
prop.ValueKind switch
|
|
{
|
|
JsonValueKind.String => prop.GetString() ?? "",
|
|
JsonValueKind.Number => prop.GetRawText(),
|
|
_ when prop.ValueKind == JsonValueKind.True || prop.ValueKind == JsonValueKind.False
|
|
=> prop.GetBoolean().ToString(),
|
|
_ => prop.GetRawText()
|
|
};
|
|
}
|