// ----------------------------------------------------------------------------- // HlcTimestampJsonConverter.cs // Sprint: SPRINT_20260105_002_001_LB_hlc_core_library // Task: HLC-006 - Add HlcTimestampJsonConverter for System.Text.Json serialization // ----------------------------------------------------------------------------- using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.HybridLogicalClock; /// /// JSON converter for HlcTimestamp using the sortable string format. /// /// /// Serializes HlcTimestamp to/from the sortable string format (e.g., "1704067200000-scheduler-east-1-000042"). /// This format is both human-readable and lexicographically sortable. /// public sealed class HlcTimestampJsonConverter : JsonConverter { /// public override HlcTimestamp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { throw new JsonException("Cannot deserialize null to HlcTimestamp. Use HlcTimestamp? with NullableHlcTimestampJsonConverter for nullable timestamps."); } if (reader.TokenType != JsonTokenType.String) { throw new JsonException($"Expected string but got {reader.TokenType}"); } var value = reader.GetString(); if (string.IsNullOrWhiteSpace(value)) { throw new JsonException("Cannot convert empty string to HlcTimestamp"); } try { return HlcTimestamp.Parse(value); } catch (FormatException ex) { throw new JsonException($"Invalid HlcTimestamp format: {value}", ex); } } /// public override void Write(Utf8JsonWriter writer, HlcTimestamp value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToSortableString()); } } /// /// JSON converter for nullable HlcTimestamp. /// public sealed class NullableHlcTimestampJsonConverter : JsonConverter { private readonly HlcTimestampJsonConverter _inner = new(); /// public override HlcTimestamp? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } return _inner.Read(ref reader, typeof(HlcTimestamp), options); } /// public override void Write(Utf8JsonWriter writer, HlcTimestamp? value, JsonSerializerOptions options) { if (!value.HasValue) { writer.WriteNullValue(); return; } _inner.Write(writer, value.Value, options); } } /// /// JSON converter for HlcTimestamp using object format with individual properties. /// /// /// Alternative converter that serializes HlcTimestamp as an object: /// /// { /// "physicalTime": 1704067200000, /// "nodeId": "scheduler-east-1", /// "logicalCounter": 42 /// } /// /// Use this when you need to query individual fields in JSON storage. /// public sealed class HlcTimestampObjectJsonConverter : JsonConverter { /// public override HlcTimestamp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException($"Expected StartObject but got {reader.TokenType}"); } long? physicalTime = null; string? nodeId = null; int? logicalCounter = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException($"Expected PropertyName but got {reader.TokenType}"); } var propertyName = reader.GetString(); reader.Read(); switch (propertyName) { case "physicalTime": case "PhysicalTime": physicalTime = reader.GetInt64(); break; case "nodeId": case "NodeId": nodeId = reader.GetString(); break; case "logicalCounter": case "LogicalCounter": logicalCounter = reader.GetInt32(); break; default: reader.Skip(); break; } } if (!physicalTime.HasValue) throw new JsonException("Missing required property 'physicalTime'"); if (string.IsNullOrEmpty(nodeId)) throw new JsonException("Missing required property 'nodeId'"); if (!logicalCounter.HasValue) throw new JsonException("Missing required property 'logicalCounter'"); return new HlcTimestamp { PhysicalTime = physicalTime.Value, NodeId = nodeId, LogicalCounter = logicalCounter.Value }; } /// public override void Write(Utf8JsonWriter writer, HlcTimestamp value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber("physicalTime", value.PhysicalTime); writer.WriteString("nodeId", value.NodeId); writer.WriteNumber("logicalCounter", value.LogicalCounter); writer.WriteEndObject(); } }