< 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: 148
Uncovered lines: 0
Coverable lines: 148
Total lines: 319
Line coverage: 100%
Branch coverage
93%
Covered branches: 62
Total branches: 66
Branch coverage: 93.9%
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(...)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 System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Text;
 5using System.Text.Json;
 6
 7namespace Chronicler;
 8
 9/// <summary>
 10/// Serializes <see cref="IRecordable"/> state graphs to and from JSON through the chronicler API.
 11/// </summary>
 12public static class JsonRecordSerializer
 13{
 114    private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions();
 15
 16    /// <summary>
 17    /// Serializes the current state of a recordable instance into JSON.
 18    /// </summary>
 19    public static string Serialize(IRecordable target, bool writeIndented = false)
 420        => Serialize(target, context: null, writeIndented);
 21
 22    /// <summary>
 23    /// Serializes the current state of a recordable instance into JSON.
 24    /// </summary>
 25    public static string Serialize(IRecordable target, ChronicleContext? context, bool writeIndented = false)
 26    {
 2727        if (target == null)
 128            throw new ArgumentNullException(nameof(target));
 29
 2630        context ??= new ChronicleContext();
 31
 2632        JsonSerializerOptions options = writeIndented
 2633            ? CreateIndentedOptions()
 2634            : _defaultOptions;
 35
 2636        var chronicler = new JsonRecordWriter(options, context);
 2637        target.RecordData(chronicler);
 2538        return chronicler.ToJson();
 39    }
 40
 41    /// <summary>
 42    /// Loads JSON state into an existing recordable instance.
 43    /// </summary>
 44    public static void Populate(IRecordable target, string json)
 645        => Populate(target, json, context: null);
 46
 47    /// <summary>
 48    /// Loads JSON state into an existing recordable instance.
 49    /// </summary>
 50    public static void Populate(IRecordable target, string json, ChronicleContext? context)
 51    {
 2752        if (target == null)
 153            throw new ArgumentNullException(nameof(target));
 2654        if (string.IsNullOrWhiteSpace(json))
 355            throw new ArgumentException("Serialized JSON must not be null or empty.", nameof(json));
 56
 2357        context ??= new ChronicleContext();
 58
 2359        using var chronicler = new JsonRecordReader(json, _defaultOptions, context);
 2360        target.RecordData(chronicler);
 2061        context.ResolveDeferredLinks();
 3662    }
 63
 64    private static JsonSerializerOptions CreateDefaultOptions()
 65    {
 166        return new JsonSerializerOptions()
 167        {
 168            IncludeFields = true
 169        };
 70    }
 71
 72    private static JsonSerializerOptions CreateIndentedOptions()
 73    {
 2374        return new JsonSerializerOptions(_defaultOptions)
 2375        {
 2376            WriteIndented = true
 2377        };
 78    }
 79
 80    private sealed class JsonRecordWriter : IChronicler
 81    {
 4182        private readonly OrderedStringMap<string> _entries = new(8, StringComparer.Ordinal);
 83        private readonly JsonSerializerOptions _options;
 84
 4185        public JsonRecordWriter(JsonSerializerOptions options, ChronicleContext context)
 86        {
 4187            _options = options;
 4188            Context = context ?? throw new ArgumentNullException(nameof(context));
 4189        }
 90
 91        public ChronicleContext Context { get; }
 92
 1693        public SerializationMode Mode => SerializationMode.Saving;
 94
 95        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 96        {
 4397            if (value is null || EqualityComparer<T>.Default.Equals(value, defaultValue!))
 998                return;
 99
 34100            _entries[name] = JsonSerializer.Serialize(value, _options);
 34101        }
 102
 103        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 104        {
 10105            if (value == null)
 106            {
 1107                _entries[name] = "null";
 1108                return;
 109            }
 110
 9111            var nested = new JsonRecordWriter(_options, Context);
 9112            value.RecordData(nested);
 9113            _entries[name] = nested.ToJson();
 9114        }
 115
 116        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 117        {
 4118            var nested = new JsonRecordWriter(_options, Context);
 4119            value.RecordData(nested);
 4120            _entries[name] = nested.ToJson();
 4121        }
 122
 123        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 124        {
 4125            if (!value.HasValue)
 2126                return;
 127
 2128            T nestedValue = value.Value;
 2129            var nested = new JsonRecordWriter(_options, Context);
 2130            nestedValue.RecordData(nested);
 2131            _entries[name] = nested.ToJson();
 2132        }
 133
 134        public void LookLink<T>(
 135            ref T value,
 136            string name,
 137            string? slot = null,
 138            RecordLinkResolveMode resolveMode = RecordLinkResolveMode.Immediate,
 139            Action<T>? assignLoadedValue = null)
 140        {
 12141            string? id = null;
 12142            if (value is not null
 12143                && !Context.Links.TryGetReferenceId(value, out id, slot))
 144            {
 1145                throw new InvalidOperationException(
 1146                    $"Unable to save link '{name}' of type {typeof(T).Name} because no stable id could be produced{Forma
 147            }
 148
 11149            _entries[name] = JsonSerializer.Serialize(id, _options);
 11150        }
 151
 152        public string ToJson()
 153        {
 40154            using var stream = new MemoryStream();
 40155            using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions() { Indented = _options.WriteIndented }
 156            {
 40157                writer.WriteStartObject();
 158
 200159                foreach (var entry in _entries)
 160                {
 60161                    writer.WritePropertyName(entry.Key);
 60162                    using var document = JsonDocument.Parse(entry.Value);
 60163                    document.RootElement.WriteTo(writer);
 164                }
 165
 40166                writer.WriteEndObject();
 40167            }
 168
 40169            return Encoding.UTF8.GetString(stream.ToArray());
 40170        }
 171    }
 172
 173    private sealed class JsonRecordReader : IChronicler, IDisposable
 174    {
 175        private readonly JsonDocument _document;
 176        private readonly JsonElement _root;
 177        private readonly JsonSerializerOptions _options;
 178
 38179        public JsonRecordReader(string json, JsonSerializerOptions options, ChronicleContext context)
 180        {
 38181            _document = JsonDocument.Parse(json);
 38182            _root = _document.RootElement;
 38183            _options = options;
 38184            Context = context ?? throw new ArgumentNullException(nameof(context));
 38185        }
 186
 187        public ChronicleContext Context { get; }
 188
 12189        public SerializationMode Mode => SerializationMode.Loading;
 190
 191        public void LookValue<T>(ref T value, string name, T? defaultValue = default)
 192        {
 40193            if (!_root.TryGetProperty(name, out JsonElement entry))
 194            {
 20195                value = defaultValue!;
 20196                return;
 197            }
 198
 20199            if (entry.ValueKind == JsonValueKind.Null)
 200            {
 1201                value = defaultValue!;
 1202                return;
 203            }
 204
 19205            T? loadedValue = JsonSerializer.Deserialize<T>(entry.GetRawText(), _options);
 19206            if (loadedValue is null)
 207            {
 1208                value = defaultValue!;
 1209                return;
 210            }
 211
 18212            value = loadedValue;
 18213        }
 214
 215        public void LookDeep<T>(ref T value, string name) where T : class, IRecordable
 216        {
 9217            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 2218                return;
 219
 7220            if (value == null)
 1221                throw new InvalidOperationException(
 1222                    $"Unable to load '{name}' because {typeof(T).Name} must already be instantiated for a deep chronicle
 223
 6224            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 6225            value.RecordData(nested);
 12226        }
 227
 228        public void LookDeepStruct<T>(ref T value, string name) where T : struct, IRecordable
 229        {
 4230            value = CreateDefaultDeepStruct<T>();
 231
 4232            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 1233                return;
 234
 3235            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 3236            value.RecordData(nested);
 6237        }
 238
 239        public void LookNullableDeep<T>(ref T? value, string name) where T : struct, IRecordable
 240        {
 4241            if (!_root.TryGetProperty(name, out JsonElement entry) || entry.ValueKind == JsonValueKind.Null)
 242            {
 3243                value = null;
 3244                return;
 245            }
 246
 1247            T nestedValue = CreateDefaultDeepStruct<T>();
 1248            using var nested = new JsonRecordReader(entry.GetRawText(), _options, Context);
 1249            nestedValue.RecordData(nested);
 1250            value = nestedValue;
 2251        }
 252
 253        public void LookLink<T>(
 254            ref T value,
 255            string name,
 256            string? slot = null,
 257            RecordLinkResolveMode resolveMode = RecordLinkResolveMode.Immediate,
 258            Action<T>? assignLoadedValue = null)
 259        {
 10260            if (!_root.TryGetProperty(name, out JsonElement entry)
 10261                || entry.ValueKind == JsonValueKind.Null)
 262            {
 2263                value = default!;
 2264                return;
 265            }
 266
 8267            string id = JsonSerializer.Deserialize<string>(entry.GetRawText(), _options)!;
 268
 8269            if (resolveMode == RecordLinkResolveMode.Deferred)
 270            {
 5271                if (assignLoadedValue == null)
 1272                    throw new InvalidOperationException(
 1273                        $"Deferred link '{name}' of type {typeof(T).Name} requires an assignment callback.");
 274
 275                T? deferredValue;
 4276                if (Context.Links.TryResolve(id, out deferredValue, slot))
 277                {
 1278                    value = deferredValue!;
 1279                    assignLoadedValue(deferredValue!);
 1280                    return;
 281                }
 282
 3283                Context.QueueDeferredLink(name, id, slot, assignLoadedValue);
 3284                value = default!;
 3285                return;
 286            }
 287
 288            T? resolvedValue;
 3289            if (!Context.Links.TryResolve(id, out resolvedValue, slot))
 290            {
 1291                throw new InvalidOperationException(
 1292                    $"Unable to load link '{name}' of type {typeof(T).Name} with id '{id}'{FormatSlot(slot)}.");
 293            }
 294
 2295            value = resolvedValue!;
 2296            assignLoadedValue?.Invoke(resolvedValue!);
 2297        }
 298
 299        public void Dispose()
 300        {
 38301            _document.Dispose();
 38302        }
 303
 304        private T CreateDefaultDeepStruct<T>() where T : struct, IRecordable
 305        {
 5306            T defaultValue = new();
 5307            using var nested = new JsonRecordReader("{}", _options, Context);
 5308            defaultValue.RecordData(nested);
 5309            return defaultValue;
 5310        }
 311    }
 312
 313    private static string FormatSlot(string? slot)
 314    {
 2315        return string.IsNullOrEmpty(slot)
 2316            ? string.Empty
 2317            : $" in slot '{slot}'";
 318    }
 319}