< Summary

Information
Class: NostrSure.Infrastructure.Serialization.NostrEventJsonConverter
Assembly: NostrSure.Infrastructure
File(s): /home/runner/work/NostrSure/NostrSure/NostrSure.Infrastructure/Serialization/NostrEventJsonConverter.cs
Line coverage
93%
Covered lines: 156
Uncovered lines: 10
Coverable lines: 166
Total lines: 247
Line coverage: 93.9%
Branch coverage
76%
Covered branches: 61
Total branches: 80
Branch coverage: 76.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_IdPropertyName()100%11100%
get_PubkeyPropertyName()100%11100%
get_CreatedAtPropertyName()100%11100%
get_KindPropertyName()100%11100%
get_TagsPropertyName()100%11100%
get_ContentPropertyName()100%11100%
get_SigPropertyName()100%11100%
.cctor()100%11100%
CanConvert(...)100%11100%
Read(...)73.07%535293.54%
ReadTagsOptimized(...)88.88%181892.85%
Write(...)50%66100%
WriteTagsOptimized(...)100%44100%
ThrowInvalidJsonException()100%11100%
ThrowMissingRequiredFieldsException()100%210%
ThrowUnknownEventKindException(...)100%210%

File(s)

/home/runner/work/NostrSure/NostrSure/NostrSure.Infrastructure/Serialization/NostrEventJsonConverter.cs

#LineLine coverage
 1using NostrSure.Domain.Entities;
 2using NostrSure.Domain.ValueObjects;
 3using System.Runtime.CompilerServices;
 4using System.Text.Json;
 5using System.Text.Json.Serialization;
 6
 7namespace NostrSure.Infrastructure.Serialization;
 8
 9public sealed class NostrEventJsonConverter : JsonConverter<NostrEvent>
 10{
 11    // Pre-allocated property name spans for faster comparison
 16312    private static ReadOnlySpan<byte> IdPropertyName => "id"u8;
 14113    private static ReadOnlySpan<byte> PubkeyPropertyName => "pubkey"u8;
 11914    private static ReadOnlySpan<byte> CreatedAtPropertyName => "created_at"u8;
 9715    private static ReadOnlySpan<byte> KindPropertyName => "kind"u8;
 7516    private static ReadOnlySpan<byte> TagsPropertyName => "tags"u8;
 5417    private static ReadOnlySpan<byte> ContentPropertyName => "content"u8;
 3218    private static ReadOnlySpan<byte> SigPropertyName => "sig"u8;
 19
 20    // Valid EventKind values for fast lookup
 121    private static readonly HashSet<int> ValidEventKinds = new()
 122    {
 123        (int)EventKind.Note,
 124        (int)EventKind.ContactList,
 125        (int)EventKind.Zap
 126    };
 27
 28    public override bool CanConvert(Type typeToConvert)
 24229    {
 24230        return typeof(NostrEvent).IsAssignableFrom(typeToConvert);
 24231    }
 32
 33    public override NostrEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 2234    {
 2235        if (reader.TokenType != JsonTokenType.StartObject)
 036            ThrowInvalidJsonException();
 37
 2238        string? id = null;
 2239        string? pubkey = null;
 2240        long? createdAt = null;
 2241        int? kindInt = null;
 2242        List<NostrTag>? tags = null;
 2243        string? content = null;
 2244        string? sig = null;
 45
 2246        var fieldsFound = 0;
 47
 17548        while (reader.Read())
 17549        {
 17550            if (reader.TokenType == JsonTokenType.EndObject)
 2051                break;
 52
 15553            if (reader.TokenType != JsonTokenType.PropertyName)
 054                ThrowInvalidJsonException();
 55
 56            // Use ValueSpan for zero-allocation property name comparison
 15557            var propertyNameSpan = reader.ValueSpan;
 15558            reader.Read();
 59
 15460            if (propertyNameSpan.SequenceEqual(IdPropertyName))
 2261            {
 2262                id = reader.GetString();
 2263                fieldsFound++;
 2264            }
 13265            else if (propertyNameSpan.SequenceEqual(PubkeyPropertyName))
 2266            {
 2267                pubkey = reader.GetString();
 2268                fieldsFound++;
 2269            }
 11070            else if (propertyNameSpan.SequenceEqual(CreatedAtPropertyName))
 2271            {
 2272                createdAt = reader.GetInt64();
 2273                fieldsFound++;
 2274            }
 8875            else if (propertyNameSpan.SequenceEqual(KindPropertyName))
 2276            {
 2277                kindInt = reader.GetInt32();
 2278                fieldsFound++;
 2279            }
 6680            else if (propertyNameSpan.SequenceEqual(TagsPropertyName))
 2181            {
 2182                if (reader.TokenType == JsonTokenType.Null)
 183                {
 184                    tags = new List<NostrTag>();
 185                }
 86                else
 2087                {
 2088                    tags = ReadTagsOptimized(ref reader);
 1989                }
 2090                fieldsFound++;
 2091            }
 4592            else if (propertyNameSpan.SequenceEqual(ContentPropertyName))
 2293            {
 2294                if (reader.TokenType == JsonTokenType.Null)
 195                {
 196                    content = string.Empty;
 197                }
 98                else
 2199                {
 21100                    content = reader.GetString();
 21101                }
 22102                fieldsFound++;
 22103            }
 23104            else if (propertyNameSpan.SequenceEqual(SigPropertyName))
 22105            {
 22106                sig = reader.GetString();
 22107                fieldsFound++;
 22108            }
 109            else
 1110            {
 1111                reader.Skip();
 1112            }
 153113        }
 114
 115        // Validate all required fields are present and handle nullable types properly
 116        // We need at minimum: id, pubkey, created_at, kind, tags
 117        // content and sig are always written but can be empty strings
 20118        if (fieldsFound < 5 ||
 20119            id is null || pubkey is null || createdAt is null ||
 20120            kindInt is null || tags is null)
 0121        {
 0122            ThrowMissingRequiredFieldsException();
 0123        }
 124
 125        // Fast EventKind validation using HashSet lookup
 20126        if (!ValidEventKinds.Contains(kindInt!.Value))
 0127            ThrowUnknownEventKindException(kindInt.Value);
 128
 20129        var eventKind = (EventKind)kindInt.Value;
 20130        var baseEvent = new NostrEvent(
 20131            id ?? string.Empty,
 20132            new Pubkey(pubkey ?? string.Empty),
 20133            DateTimeOffset.FromUnixTimeSeconds(createdAt!.Value),
 20134            eventKind,
 20135            tags ?? new List<NostrTag>(),
 20136            content ?? string.Empty,
 20137            sig ?? string.Empty
 20138        );
 139
 140        // For ContactList events, return ContactListEvent with extracted contacts
 20141        if (eventKind == EventKind.ContactList)
 5142        {
 5143            return ContactListEvent.FromNostrEvent(baseEvent);
 144        }
 145
 15146        return baseEvent;
 20147    }
 148
 149    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 150    private static List<NostrTag> ReadTagsOptimized(ref Utf8JsonReader reader)
 20151    {
 20152        if (reader.TokenType != JsonTokenType.StartArray)
 0153            ThrowInvalidJsonException();
 154
 20155        var tags = new List<NostrTag>();
 156
 46157        while (reader.Read())
 46158        {
 46159            if (reader.TokenType == JsonTokenType.EndArray)
 19160                break;
 161
 27162            if (reader.TokenType != JsonTokenType.StartArray)
 1163                ThrowInvalidJsonException();
 164
 165            // Pre-allocate list for tag elements
 26166            var tagElements = new List<string>();
 167
 91168            while (reader.Read())
 91169            {
 91170                if (reader.TokenType == JsonTokenType.EndArray)
 26171                    break;
 172
 65173                if (reader.TokenType != JsonTokenType.String)
 0174                    ThrowInvalidJsonException();
 175
 65176                var value = reader.GetString();
 65177                if (value is not null)
 65178                    tagElements.Add(value);
 65179            }
 180
 181            // Only create NostrTag if we have at least one element (the tag name)
 26182            if (tagElements.Count > 0)
 26183            {
 184                // Use NostrTag.FromArray for proper validation and construction
 26185                tags.Add(NostrTag.FromArray(tagElements));
 26186            }
 26187        }
 188
 19189        return tags;
 19190    }
 191
 192    public override void Write(Utf8JsonWriter writer, NostrEvent value, JsonSerializerOptions options)
 9193    {
 9194        writer.WriteStartObject();
 195
 196        // Write properties in optimal order (most common lookups first)
 9197        writer.WriteString(IdPropertyName, value.Id);
 9198        writer.WriteString(PubkeyPropertyName, value.Pubkey.Value);
 9199        writer.WriteNumber(CreatedAtPropertyName, value.CreatedAt.ToUnixTimeSeconds());
 9200        writer.WriteNumber(KindPropertyName, (int)value.Kind);
 201
 202        // Inline tags serialization for better performance
 9203        writer.WritePropertyName(TagsPropertyName);
 9204        WriteTagsOptimized(writer, value.Tags ?? new List<NostrTag>());
 205
 9206        writer.WriteString(ContentPropertyName, value.Content ?? string.Empty);
 9207        writer.WriteString(SigPropertyName, value.Sig ?? string.Empty);
 208
 9209        writer.WriteEndObject();
 9210    }
 211
 212    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 213    private static void WriteTagsOptimized(Utf8JsonWriter writer, IReadOnlyList<NostrTag> tags)
 9214    {
 9215        writer.WriteStartArray();
 216
 35217        foreach (var tag in tags)
 4218        {
 4219            writer.WriteStartArray();
 220
 221            // Write tag name first
 4222            writer.WriteStringValue(tag.Name);
 223
 224            // Write tag values
 30225            foreach (var value in tag.Values)
 9226            {
 9227                writer.WriteStringValue(value);
 9228            }
 229
 4230            writer.WriteEndArray();
 4231        }
 232
 9233        writer.WriteEndArray();
 9234    }
 235
 236    [MethodImpl(MethodImplOptions.NoInlining)]
 237    private static void ThrowInvalidJsonException() =>
 1238        throw new JsonException("Invalid JSON format for NostrEvent");
 239
 240    [MethodImpl(MethodImplOptions.NoInlining)]
 241    private static void ThrowMissingRequiredFieldsException() =>
 0242        throw new JsonException("Missing required NostrEvent fields");
 243
 244    [MethodImpl(MethodImplOptions.NoInlining)]
 245    private static void ThrowUnknownEventKindException(int kind) =>
 0246        throw new JsonException($"Unknown event kind: {kind}");
 247}