< 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: 156
Uncovered lines: 0
Coverable lines: 156
Total lines: 346
Line coverage: 100%
Branch coverage
94%
Covered branches: 64
Total branches: 68
Branch coverage: 94.1%
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_Mode()100%11100%
LookValue(...)100%44100%
LookDeep(...)100%22100%
LookDeepStruct(...)100%11100%
LookNullableDeep(...)100%22100%
LookLink(...)100%44100%
ToJson()100%22100%
.ctor(...)50%22100%
get_Mode()100%11100%
LookValue(...)100%66100%
LookDeep(...)100%66100%
LookDeepStruct(...)100%44100%
LookNullableDeep(...)100%44100%
LookLink(...)100%44100%
TryReadLinkId(...)100%44100%
LoadDeferredLink(...)100%44100%
LoadImmediateLink(...)75%44100%
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 System;
 2using System.Collections.Generic;
 3using System.Diagnostics.CodeAnalysis;
 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)
 521        => 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)
 27    {
 2928        if (target == null)
 129            throw new ArgumentNullException(nameof(target));
 30
 2831        context ??= new ChronicleContext();
 32
 2833        JsonSerializerOptions options = writeIndented
 2834            ? CreateIndentedOptions()
 2835            : _defaultOptions;
 36
 2837        var chronicler = new JsonRecordWriter(options, context);
 2838        target.RecordData(chronicler);
 2739        return chronicler.ToJson();
 40    }
 41
 42    /// <summary>
 43    /// Loads JSON state into an existing recordable instance.
 44    /// </summary>
 45    public static void Populate(IRecordable target, string json)
 746        => 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)
 52    {
 2953        if (target == null)
 154            throw new ArgumentNullException(nameof(target));
 2855        if (string.IsNullOrWhiteSpace(json))
 356            throw new ArgumentException("Serialized JSON must not be null or empty.", nameof(json));
 57
 2558        context ??= new ChronicleContext();
 59
 2560        using var chronicler = new JsonRecordReader(json, _defaultOptions, context);
 2561        target.RecordData(chronicler);
 2262        context.ResolveDeferredLinks();
 4063    }
 64
 65    private static JsonSerializerOptions CreateDefaultOptions()
 66    {
 167        return new JsonSerializerOptions()
 168        {
 169            IncludeFields = true
 170        };
 71    }
 72
 73    private static JsonSerializerOptions CreateIndentedOptions()
 74    {
 2575        return new JsonSerializerOptions(_defaultOptions)
 2576        {
 2577            WriteIndented = true
 2578        };
 79    }
 80
 81    private sealed class JsonRecordWriter : IChronicler
 82    {
 4783        private readonly OrderedStringMap<string> _entries = new(8, StringComparer.Ordinal);
 84        private readonly JsonSerializerOptions _options;
 85
 4786        public JsonRecordWriter(JsonSerializerOptions options, ChronicleContext context)
 87        {
 4788            _options = options;
 4789            Context = context ?? throw new ArgumentNullException(nameof(context));
 4790        }
 91
 92        public ChronicleContext Context { get; }
 93
 1794        public SerializationMode Mode => SerializationMode.Saving;
 95
 96        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 97        {
 5098            if (value is null || EqualityComparer<T>.Default.Equals(value, defaultValue!))
 1099                return;
 100
 40101            _entries[name] = JsonSerializer.Serialize(value, _options);
 40102        }
 103
 104        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 105        {
 12106            if (value == null)
 107            {
 1108                _entries[name] = "null";
 1109                return;
 110            }
 111
 11112            var nested = new JsonRecordWriter(_options, Context);
 11113            value.RecordData(nested);
 11114            _entries[name] = nested.ToJson();
 11115        }
 116
 117        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 118        {
 5119            var nested = new JsonRecordWriter(_options, Context);
 5120            value.RecordData(nested);
 5121            _entries[name] = nested.ToJson();
 5122        }
 123
 124        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 125        {
 5126            if (!value.HasValue)
 2127                return;
 128
 3129            T nestedValue = value.Value;
 3130            var nested = new JsonRecordWriter(_options, Context);
 3131            nestedValue.RecordData(nested);
 3132            _entries[name] = nested.ToJson();
 3133        }
 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)
 141        {
 12142            string? id = null;
 12143            if (value is not null
 12144                && !Context.Links.TryGetReferenceId(value, out id, slot))
 145            {
 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()
 154        {
 46155            using var stream = new MemoryStream();
 46156            using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions() { Indented = _options.WriteIndented }
 157            {
 46158                writer.WriteStartObject();
 159
 232160                foreach (var entry in _entries)
 161                {
 70162                    writer.WritePropertyName(entry.Key);
 70163                    using var document = JsonDocument.Parse(entry.Value);
 70164                    document.RootElement.WriteTo(writer);
 165                }
 166
 46167                writer.WriteEndObject();
 46168            }
 169
 46170            return Encoding.UTF8.GetString(stream.ToArray());
 46171        }
 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
 46180        public JsonRecordReader(string json, JsonSerializerOptions options, ChronicleContext context)
 181        {
 46182            _document = JsonDocument.Parse(json);
 46183            _root = _document.RootElement;
 46184            _options = options;
 46185            Context = context ?? throw new ArgumentNullException(nameof(context));
 46186        }
 187
 188        public ChronicleContext Context { get; }
 189
 13190        public SerializationMode Mode => SerializationMode.Loading;
 191
 192        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 193        {
 49194            if (!_root.TryGetProperty(name, out JsonElement entry))
 195            {
 24196                value = defaultValue!;
 24197                return;
 198            }
 199
 25200            if (entry.ValueKind == JsonValueKind.Null)
 201            {
 1202                value = defaultValue!;
 1203                return;
 204            }
 205
 24206            T? loadedValue = JsonSerializer.Deserialize<T>(entry.GetRawText(), _options);
 24207            if (loadedValue is null)
 208            {
 1209                value = defaultValue!;
 1210                return;
 211            }
 212
 23213            value = loadedValue;
 23214        }
 215
 216        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 217        {
 11218            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 2219                return;
 220
 9221            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
 8225            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 8226            value.RecordData(nested);
 16227        }
 228
 229        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 230        {
 5231            value = CreateDefaultDeepStruct<T>();
 232
 5233            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 1234                return;
 235
 4236            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 4237            value.RecordData(nested);
 8238        }
 239
 240        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 241        {
 5242            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 243            {
 3244                value = null;
 3245                return;
 246            }
 247
 2248            T nestedValue = CreateDefaultDeepStruct<T>();
 2249            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 2250            nestedValue.RecordData(nested);
 2251            value = nestedValue;
 4252        }
 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)
 260        {
 10261            if (!TryReadLinkId(name, out string? id))
 262            {
 2263                value = default!;
 2264                return;
 265            }
 266
 8267            if (resolveMode == RecordLinkResolveMode.Deferred)
 268            {
 5269                LoadDeferredLink(ref value, name, id, slot, assignLoadedValue);
 4270                return;
 271            }
 272
 3273            LoadImmediateLink(ref value, name, id, slot, assignLoadedValue);
 2274        }
 275
 276        private bool TryReadLinkId(string name, [NotNullWhen(true)] out string? id)
 277        {
 10278            if (!_root.TryGetProperty(name, out JsonElement entry)
 10279                || entry.ValueKind == JsonValueKind.Null)
 280            {
 2281                id = null;
 2282                return false;
 283            }
 284
 8285            id = JsonSerializer.Deserialize<string>(entry.GetRawText(), _options);
 8286            return id != null;
 287        }
 288
 289        private void LoadDeferredLink<T>(
 290            ref T value,
 291            string name,
 292            string id,
 293            string? slot,
 294            Action<T>? assignLoadedValue)
 295        {
 5296            if (assignLoadedValue == null)
 1297                throw new InvalidOperationException(
 1298                    $"Deferred link '{name}' of type {typeof(T).Name} requires an assignment callback.");
 299
 4300            if (Context.Links.TryResolve(id, out T? deferredValue, slot))
 301            {
 1302                value = deferredValue!;
 1303                assignLoadedValue(deferredValue!);
 1304                return;
 305            }
 306
 3307            Context.QueueDeferredLink(name, id, slot, assignLoadedValue);
 3308            value = default!;
 3309        }
 310
 311        private void LoadImmediateLink<T>(
 312            ref T value,
 313            string name,
 314            string id,
 315            string? slot,
 316            Action<T>? assignLoadedValue)
 317        {
 3318            if (!Context.Links.TryResolve(id, out T? resolvedValue, slot))
 1319                throw new InvalidOperationException(
 1320                    $"Unable to load link '{name}' of type {typeof(T).Name} with id '{id}'{FormatSlot(slot)}.");
 321
 2322            value = resolvedValue!;
 2323            assignLoadedValue?.Invoke(resolvedValue!);
 2324        }
 325
 326        public void Dispose()
 327        {
 46328            _document.Dispose();
 46329        }
 330
 331        private T CreateDefaultDeepStruct<T>() where T : struct, IRecordable
 332        {
 7333            T defaultValue = new();
 7334            using var nested = new JsonRecordReader("{}", _options, Context);
 7335            defaultValue.RecordData(nested);
 7336            return defaultValue;
 7337        }
 338    }
 339
 340    private static string FormatSlot(string? slot)
 341    {
 2342        return string.IsNullOrEmpty(slot)
 2343            ? string.Empty
 2344            : $" in slot '{slot}'";
 345    }
 346}