< Summary

Information
Class: Chronicler.JsonRecordSerializer
Assembly: Chronicler
File(s): /home/runner/work/Chronicler/Chronicler/src/Chronicler/Serialization/Json/JsonRecordSerializer.cs
Line coverage
100%
Covered lines: 187
Uncovered lines: 0
Coverable lines: 187
Total lines: 320
Line coverage: 100%
Branch coverage
92%
Covered branches: 61
Total branches: 66
Branch coverage: 92.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Serialize(...)100%11100%
Serialize(...)100%66100%
Populate(...)100%11100%
Populate(...)100%66100%
CreateDefaultOptions()100%11100%
CreateIndentedOptions()100%11100%
.ctor(...)50%22100%
get_Context()100%11100%
get_Mode()100%11100%
LookValue(...)100%44100%
LookDeep(...)100%22100%
LookDeepStruct(...)100%11100%
LookNullableDeep(...)100%22100%
LookLink(...)75%44100%
ToJson()100%22100%
.ctor(...)50%22100%
get_Context()100%11100%
get_Mode()100%11100%
LookValue(...)100%66100%
LookDeep(...)100%66100%
LookDeepStruct(...)100%44100%
LookNullableDeep(...)100%44100%
LookLink(...)92.85%1414100%
Dispose()100%11100%
CreateDefaultDeepStruct()100%11100%
FormatSlot(...)50%22100%

File(s)

/home/runner/work/Chronicler/Chronicler/src/Chronicler/Serialization/Json/JsonRecordSerializer.cs

#LineLine coverage
 1using SwiftCollections;
 2using System;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Text;
 6using System.Text.Json;
 7
 8namespace Chronicler;
 9
 10/// <summary>
 11/// Serializes <see cref="IRecordable"/> state graphs to and from JSON through the chronicler API.
 12/// </summary>
 13public static class JsonRecordSerializer
 14{
 115    private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions();
 16
 17    /// <summary>
 18    /// Serializes the current state of a recordable instance into JSON.
 19    /// </summary>
 20    public static string Serialize(IRecordable target, bool writeIndented = false)
 421        => Serialize(target, context: null, writeIndented);
 22
 23    /// <summary>
 24    /// Serializes the current state of a recordable instance into JSON.
 25    /// </summary>
 26    public static string Serialize(IRecordable target, ChronicleContext? context, bool writeIndented = false)
 2727    {
 2728        if (target == null)
 129            throw new ArgumentNullException(nameof(target));
 30
 2631        context ??= new ChronicleContext();
 32
 2633        JsonSerializerOptions options = writeIndented
 2634            ? CreateIndentedOptions()
 2635            : _defaultOptions;
 36
 2637        var chronicler = new JsonRecordWriter(options, context);
 2638        target.RecordData(chronicler);
 2539        return chronicler.ToJson();
 2540    }
 41
 42    /// <summary>
 43    /// Loads JSON state into an existing recordable instance.
 44    /// </summary>
 45    public static void Populate(IRecordable target, string json)
 646        => Populate(target, json, context: null);
 47
 48    /// <summary>
 49    /// Loads JSON state into an existing recordable instance.
 50    /// </summary>
 51    public static void Populate(IRecordable target, string json, ChronicleContext? context)
 2752    {
 2753        if (target == null)
 154            throw new ArgumentNullException(nameof(target));
 2655        if (string.IsNullOrWhiteSpace(json))
 356            throw new ArgumentException("Serialized JSON must not be null or empty.", nameof(json));
 57
 2358        context ??= new ChronicleContext();
 59
 2360        using var chronicler = new JsonRecordReader(json, _defaultOptions, context);
 2361        target.RecordData(chronicler);
 2062        context.ResolveDeferredLinks();
 3663    }
 64
 65    private static JsonSerializerOptions CreateDefaultOptions()
 166    {
 167        return new JsonSerializerOptions()
 168        {
 169            IncludeFields = true
 170        };
 171    }
 72
 73    private static JsonSerializerOptions CreateIndentedOptions()
 2374    {
 2375        return new JsonSerializerOptions(_defaultOptions)
 2376        {
 2377            WriteIndented = true
 2378        };
 2379    }
 80
 81    private sealed class JsonRecordWriter : IChronicler
 82    {
 4183        private readonly SwiftDictionary<string, string> _entries = new(8, StringComparer.Ordinal);
 84        private readonly JsonSerializerOptions _options;
 85
 4186        public JsonRecordWriter(JsonSerializerOptions options, ChronicleContext context)
 4187        {
 4188            _options = options;
 4189            Context = context ?? throw new ArgumentNullException(nameof(context));
 4190        }
 91
 2792        public ChronicleContext Context { get; }
 93
 1694        public SerializationMode Mode => SerializationMode.Saving;
 95
 96        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 4397        {
 4398            if (value is null || EqualityComparer<T>.Default.Equals(value, defaultValue!))
 999                return;
 100
 34101            _entries[name] = JsonSerializer.Serialize(value, _options);
 43102        }
 103
 104        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 10105        {
 10106            if (value == null)
 1107            {
 1108                _entries[name] = "null";
 1109                return;
 110            }
 111
 9112            var nested = new JsonRecordWriter(_options, Context);
 9113            value.RecordData(nested);
 9114            _entries[name] = nested.ToJson();
 10115        }
 116
 117        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 4118        {
 4119            var nested = new JsonRecordWriter(_options, Context);
 4120            value.RecordData(nested);
 4121            _entries[name] = nested.ToJson();
 4122        }
 123
 124        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 4125        {
 4126            if (!value.HasValue)
 2127                return;
 128
 2129            T nestedValue = value.Value;
 2130            var nested = new JsonRecordWriter(_options, Context);
 2131            nestedValue.RecordData(nested);
 2132            _entries[name] = nested.ToJson();
 4133        }
 134
 135        public void LookLink<T>(
 136            ref T value,
 137            string name,
 138            string? slot = null,
 139            RecordLinkResolveMode resolveMode = RecordLinkResolveMode.Immediate,
 140            Action<T>? assignLoadedValue = null)
 12141        {
 12142            string? id = null;
 12143            if (value is not null
 12144                && !Context.Links.TryGetReferenceId(value, out id, slot))
 1145            {
 1146                throw new InvalidOperationException(
 1147                    $"Unable to save link '{name}' of type {typeof(T).Name} because no stable id could be produced{Forma
 148            }
 149
 11150            _entries[name] = JsonSerializer.Serialize(id, _options);
 11151        }
 152
 153        public string ToJson()
 40154        {
 40155            using var stream = new MemoryStream();
 40156            using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions() { Indented = _options.WriteIndented }
 40157            {
 40158                writer.WriteStartObject();
 159
 240160                foreach (var entry in _entries)
 60161                {
 60162                    writer.WritePropertyName(entry.Key);
 60163                    using var document = JsonDocument.Parse(entry.Value);
 60164                    document.RootElement.WriteTo(writer);
 60165                }
 166
 40167                writer.WriteEndObject();
 40168            }
 169
 40170            return Encoding.UTF8.GetString(stream.ToArray());
 40171        }
 172    }
 173
 174    private sealed class JsonRecordReader : IChronicler, IDisposable
 175    {
 176        private readonly JsonDocument _document;
 177        private readonly JsonElement _root;
 178        private readonly JsonSerializerOptions _options;
 179
 38180        public JsonRecordReader(string json, JsonSerializerOptions options, ChronicleContext context)
 38181        {
 38182            _document = JsonDocument.Parse(json);
 38183            _root = _document.RootElement;
 38184            _options = options;
 38185            Context = context ?? throw new ArgumentNullException(nameof(context));
 38186        }
 187
 26188        public ChronicleContext Context { get; }
 189
 12190        public SerializationMode Mode => SerializationMode.Loading;
 191
 192        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 40193        {
 40194            if (!_root.TryGetProperty(name, out JsonElement entry))
 20195            {
 20196                value = defaultValue!;
 20197                return;
 198            }
 199
 20200            if (entry.ValueKind == JsonValueKind.Null)
 1201            {
 1202                value = defaultValue!;
 1203                return;
 204            }
 205
 19206            T? loadedValue = JsonSerializer.Deserialize<T>(entry.GetRawText(), _options);
 19207            if (loadedValue is null)
 1208            {
 1209                value = defaultValue!;
 1210                return;
 211            }
 212
 18213            value = loadedValue;
 40214        }
 215
 216        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 9217        {
 9218            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 2219                return;
 220
 7221            if (value == null)
 1222                throw new InvalidOperationException(
 1223                    $"Unable to load '{name}' because {typeof(T).Name} must already be instantiated for a deep chronicle
 224
 6225            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 6226            value.RecordData(nested);
 14227        }
 228
 229        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 4230        {
 4231            value = CreateDefaultDeepStruct<T>();
 232
 4233            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 1234                return;
 235
 3236            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 3237            value.RecordData(nested);
 7238        }
 239
 240        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 4241        {
 4242            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 3243            {
 3244                value = null;
 3245                return;
 246            }
 247
 1248            T nestedValue = CreateDefaultDeepStruct<T>();
 1249            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 1250            nestedValue.RecordData(nested);
 1251            value = nestedValue;
 5252        }
 253
 254        public void LookLink<T>(
 255            ref T value,
 256            string name,
 257            string? slot = null,
 258            RecordLinkResolveMode resolveMode = RecordLinkResolveMode.Immediate,
 259            Action<T>? assignLoadedValue = null)
 10260        {
 10261            if (!_root.TryGetProperty(name, out JsonElement entry)
 10262                || entry.ValueKind == JsonValueKind.Null)
 2263            {
 2264                value = default!;
 2265                return;
 266            }
 267
 8268            string id = JsonSerializer.Deserialize<string>(entry.GetRawText(), _options)!;
 269
 8270            if (resolveMode == RecordLinkResolveMode.Deferred)
 5271            {
 5272                if (assignLoadedValue == null)
 1273                    throw new InvalidOperationException(
 1274                        $"Deferred link '{name}' of type {typeof(T).Name} requires an assignment callback.");
 275
 276                T? deferredValue;
 4277                if (Context.Links.TryResolve(id, out deferredValue, slot))
 1278                {
 1279                    value = deferredValue!;
 1280                    assignLoadedValue(deferredValue!);
 1281                    return;
 282                }
 283
 3284                Context.QueueDeferredLink(name, id, slot, assignLoadedValue);
 3285                value = default!;
 3286                return;
 287            }
 288
 289            T? resolvedValue;
 3290            if (!Context.Links.TryResolve(id, out resolvedValue, slot))
 1291            {
 1292                throw new InvalidOperationException(
 1293                    $"Unable to load link '{name}' of type {typeof(T).Name} with id '{id}'{FormatSlot(slot)}.");
 294            }
 295
 2296            value = resolvedValue!;
 2297            assignLoadedValue?.Invoke(resolvedValue!);
 8298        }
 299
 300        public void Dispose()
 38301        {
 38302            _document.Dispose();
 38303        }
 304
 305        private T CreateDefaultDeepStruct<T>() where T : struct, IRecordable
 5306        {
 5307            T defaultValue = new();
 5308            using var nested = new JsonRecordReader("{}", _options, Context);
 5309            defaultValue.RecordData(nested);
 5310            return defaultValue;
 5311        }
 312    }
 313
 314    private static string FormatSlot(string? slot)
 2315    {
 2316        return string.IsNullOrEmpty(slot)
 2317            ? string.Empty
 2318            : $" in slot '{slot}'";
 2319    }
 320}