using System.Text.Json;
using StellaOps.TimelineIndexer.Core.Models;
namespace StellaOps.TimelineIndexer.Infrastructure.Subscriptions;
///
/// Normalises incoming orchestrator/notification envelopes into instances.
///
public sealed class TimelineEnvelopeParser
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false,
PropertyNameCaseInsensitive = true
};
public bool TryParse(string rawJson, out TimelineEventEnvelope envelope, out string? failureReason)
{
if (string.IsNullOrWhiteSpace(rawJson))
{
envelope = default!;
failureReason = "Payload was empty";
return false;
}
try
{
using var doc = JsonDocument.Parse(rawJson);
var root = doc.RootElement;
var eventId = FirstString(root, "eventId", "event_id", "id", "messageId");
var tenantId = FirstString(root, "tenant", "tenantId", "tenant_id");
var eventType = FirstString(root, "kind", "eventType", "event_type", "type");
var source = FirstString(root, "source", "producer") ?? "unknown";
var correlationId = FirstString(root, "correlationId", "correlation_id");
var traceId = FirstString(root, "traceId", "trace_id");
var actor = ExtractActor(root);
var severity = (FirstString(root, "severity") ?? "info").ToLowerInvariant();
var occurredAt = FirstDateTime(root, "occurredAt", "occurred_at", "timestamp", "ts") ?? DateTimeOffset.UtcNow;
var normalizedPayload = ExtractNormalizedPayload(root);
var attributes = ExtractAttributes(root);
envelope = new TimelineEventEnvelope
{
EventId = eventId ?? throw new InvalidOperationException("event_id is required"),
TenantId = tenantId ?? throw new InvalidOperationException("tenant_id is required"),
EventType = eventType ?? throw new InvalidOperationException("event_type is required"),
Source = source,
OccurredAt = occurredAt,
CorrelationId = correlationId,
TraceId = traceId,
Actor = actor,
Severity = severity,
RawPayloadJson = JsonSerializer.Serialize(root, SerializerOptions),
NormalizedPayloadJson = normalizedPayload,
Attributes = attributes,
BundleDigest = FirstString(root, "bundleDigest"),
BundleId = FirstGuid(root, "bundleId"),
AttestationSubject = FirstString(root, "attestationSubject"),
AttestationDigest = FirstString(root, "attestationDigest"),
ManifestUri = FirstString(root, "manifestUri")
};
failureReason = null;
return true;
}
catch (Exception ex)
{
envelope = default!;
failureReason = ex.Message;
return false;
}
}
private static string? ExtractActor(JsonElement root)
{
if (TryGetProperty(root, "actor", out var actorElement))
{
if (actorElement.ValueKind == JsonValueKind.String)
{
return actorElement.GetString();
}
if (actorElement.ValueKind == JsonValueKind.Object)
{
if (actorElement.TryGetProperty("subject", out var subject) && subject.ValueKind == JsonValueKind.String)
{
return subject.GetString();
}
if (actorElement.TryGetProperty("user", out var user) && user.ValueKind == JsonValueKind.String)
{
return user.GetString();
}
}
}
return null;
}
private static string? ExtractNormalizedPayload(JsonElement root)
{
if (TryGetProperty(root, "payload", out var payload))
{
return JsonSerializer.Serialize(payload, SerializerOptions);
}
if (TryGetProperty(root, "data", out var data) && data.ValueKind is JsonValueKind.Object)
{
return JsonSerializer.Serialize(data, SerializerOptions);
}
return null;
}
private static IDictionary? ExtractAttributes(JsonElement root)
{
if (!TryGetProperty(root, "attributes", out var attributes) || attributes.ValueKind != JsonValueKind.Object)
{
return null;
}
var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var property in attributes.EnumerateObject())
{
var value = property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString(),
JsonValueKind.Number => property.Value.ToString(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
_ => null
};
if (!string.IsNullOrWhiteSpace(value))
{
dict[property.Name] = value!;
}
}
return dict.Count == 0 ? null : dict;
}
private static bool TryGetProperty(JsonElement root, string name, out JsonElement value)
{
if (root.TryGetProperty(name, out value))
{
return true;
}
foreach (var property in root.EnumerateObject())
{
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
value = default;
return false;
}
private static string? FirstString(JsonElement root, params string[] names)
{
foreach (var name in names)
{
if (TryGetProperty(root, name, out var value) && value.ValueKind == JsonValueKind.String)
{
var str = value.GetString();
if (!string.IsNullOrWhiteSpace(str))
{
return str;
}
}
}
return null;
}
private static Guid? FirstGuid(JsonElement root, params string[] names)
{
var text = FirstString(root, names);
if (Guid.TryParse(text, out var guid))
{
return guid;
}
return null;
}
private static DateTimeOffset? FirstDateTime(JsonElement root, params string[] names)
{
foreach (var name in names)
{
if (TryGetProperty(root, name, out var value) && value.ValueKind == JsonValueKind.String)
{
var text = value.GetString();
if (!string.IsNullOrWhiteSpace(text) && DateTimeOffset.TryParse(text, out var dto))
{
return dto;
}
}
}
return null;
}
}