• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zorbathut / dec / 11691562073

05 Nov 2024 07:56PM UTC coverage: 90.637% (+0.2%) from 90.487%
11691562073

push

github

zorbathut
Rename StartData to more descriptive names.

4656 of 5137 relevant lines covered (90.64%)

191886.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.6
/src/Recorder.cs
1
using System;
2
using System.Collections.Generic;
3

4
namespace Dec
5
{
6
    /// <summary>
7
    /// Base class for recordable elements.
8
    /// </summary>
9
    /// <remarks>
10
    /// Inheriting from this is the easiest way to support Recorder serialization.
11
    ///
12
    /// If you need to record a class that you can't modify the definition of, see the Converter system.
13
    /// </remarks>
14
    public interface IRecordable
15
    {
16
        /// <summary>
17
        /// Serializes or deserializes this object to Recorder.
18
        /// </summary>
19
        /// <remarks>
20
        /// This function is called both for serialization and deserialization. In most cases, you can simply call Recorder.Record functions to do the right thing.
21
        ///
22
        /// For more complicated requirements, check out Recorder's interface.
23
        /// </remarks>
24
        /// <example>
25
        /// <code>
26
        ///     public void Record(Recorder recorder)
27
        ///     {
28
        ///         // The Recorder interface figures out the right thing based on context.
29
        ///         // Any members that are referenced elsewhere will be turned into refs automatically.
30
        ///         // Members that don't show up in the saved data will be left at their default value.
31
        ///         recorder.Record(ref integerMember, "integerMember");
32
        ///         recorder.Record(ref classMember, "classMember");
33
        ///         recorder.Record(ref structMember, "structMember");
34
        ///         recorder.Record(ref collectionMember, "collectionMember");
35
        ///     }
36
        /// </code>
37
        /// </example>
38
        void Record(Recorder recorder);
39
    }
40

41
    /// <summary>
42
    /// Allows the Recordable aspect to be disabled based on local properties, usersettings, or other factors.
43
    /// </summary>
44
    public interface IConditionalRecordable : IRecordable
45
    {
46
        /// <summary>
47
        /// Indicates whether this should be treated like an IRecordable.
48
        /// </summary>
49
        /// <remarks>
50
        /// If this function returns false, Dec will either use reflection (if available) or return an error as if the object was not serializable.
51
        /// </remarks>
52
        bool ShouldRecord(Recorder.IUserSettings userSettings);
53
    }
54

55
    // This exists solely to ensure I always remember to add the right functions both to Parameter and Recorder.
56
    internal interface IRecorder
57
    {
58
        void Record<T>(ref T value, string label);
59
        void RecordAsThis<T>(ref T value);
60

61
        Recorder.Parameters WithFactory(Dictionary<Type, Func<Type, object>> factories);
62
        Recorder.Parameters Bespoke_KeyTypeDict();
63
    }
64

65
    /// <summary>
66
    /// Main class for the serialization/deserialization system.
67
    /// </summary>
68
    /// <remarks>
69
    /// Recorder is used to call the main functions for serialization/deserialization. This includes both the static initiation functions (Read, Write) and the per-element status functions.
70
    ///
71
    /// To start serializing or deserializing an object, see Recorder.Read and Recorder.Write.
72
    /// </remarks>
73
    public abstract partial class Recorder : IRecorder
74
    {
75
        public interface IUserSettings { }
76

77
        public struct Parameters : IRecorder
78
        {
79
            internal Recorder recorder;
80

81
            internal bool asThis;
82
            internal bool shared;
83

84
            internal bool bespoke_keytypedict;
85

86
            internal Dictionary<Type, Func<Type, object>> factories;
87

88
            /// <summary>
89
            /// Serialize or deserialize a member of a class.
90
            /// </summary>
91
            /// <remarks>
92
            /// See [`Dec.Recorder.Record`](xref:Dec.Recorder.Record*) for details.
93
            /// </remarks>
94
            public void Record<T>(ref T value, string label)
95
            {
1,606,635✔
96
                recorder.Record(ref value, label, this);
1,606,635✔
97
            }
1,606,635✔
98

99
            /// <summary>
100
            /// Serialize or deserialize a member of a class as if it were this class.
101
            /// </summary>
102
            /// <remarks>
103
            /// See [`Dec.Recorder.RecordAsThis`](xref:Dec.Recorder.RecordAsThis*) for details.
104
            /// </remarks>
105
            public void RecordAsThis<T>(ref T value)
106
            {
65✔
107
                Parameters parameters = this;
65✔
108
                parameters.asThis = true;
65✔
109

110
                if (parameters.shared)
65✔
111
                {
65✔
112
                    Dbg.Err("Recorder.RecordAsThis() called on Recorder.Parameters with sharing enabled. This is disallowed and sharing will be disabled.");
65✔
113
                    parameters.shared = false;
65✔
114
                }
65✔
115

116
                recorder.Record(ref value, "", parameters);
65✔
117
            }
65✔
118

119
            /// <summary>
120
            /// Add a factory layer to objects created during this call.
121
            /// </summary>
122
            /// <remarks>
123
            /// See [`Dec.Recorder.WithFactory`](xref:Dec.Recorder.WithFactory*) for details.
124
            /// </remarks>
125
            public Parameters WithFactory(Dictionary<Type, Func<Type, object>> factories)
126
            {
195✔
127
                Parameters parameters = this;
195✔
128
                if (parameters.factories != null)
195✔
129
                {
130✔
130
                    Dbg.Err("Recorder.WithFactory() called on Recorder.Parameters that already has factories. This is undefined results; currently replacing the old factory dictionary with the new one.");
130✔
131
                }
130✔
132

133
                if (parameters.shared)
195✔
134
                {
65✔
135
                    Dbg.Err("Recorder.WithFactory() called on a Shared Recorder.Parameters. This is disallowed; currently overriding Shared with factories.");
65✔
136
                    parameters.shared = false;
65✔
137
                }
65✔
138

139
                parameters.factories = factories;
195✔
140

141
                return parameters;
195✔
142
            }
195✔
143

144
            /// <summary>
145
            /// Informs the recorder system that the next parameter will be a Dictionary&lt;Type, V&gt; where the key should be interpreted as the type of the V.
146
            /// </summary>
147
            /// <remarks>
148
            /// /// See [`Dec.Recorder.Bespoke_KeyTypeDict`](xref:Dec.Recorder.Bespoke_KeyTypeDict*) for details.
149
            /// </remarks>
150
            public Recorder.Parameters Bespoke_KeyTypeDict()
151
            {
×
152
                Parameters parameters = this;
×
153
                parameters.bespoke_keytypedict = true;
×
154
                return parameters;
×
155
            }
×
156

157
            /// <summary>
158
            /// Allow sharing for class objects referenced during this call.
159
            /// </summary>
160
            /// <remarks>
161
            /// See [`Dec.Recorder.Shared`](xref:Dec.Recorder.Shared*) for details.
162
            /// </remarks>
163
            public Parameters Shared()
164
            {
65✔
165
                Parameters parameters = this;
65✔
166

167
                if (parameters.factories != null)
65✔
168
                {
65✔
169
                    Dbg.Err("Recorder.Shared() called on a WithFactory Recorder.Parameters. This is disallowed; currently erasing the factory and falling back on Shared.");
65✔
170
                    parameters.factories = null;
65✔
171
                }
65✔
172

173
                parameters.shared = true;
65✔
174
                return parameters;
65✔
175
            }
65✔
176

177
            /// <summary>
178
            /// Indicates whether this Recorder is being used for reading or writing.
179
            /// </summary>
180
            public Direction Mode { get => recorder.Mode; }
×
181

182
            internal Settings CreateSettings()
183
            {
2,033,480✔
184
                return new Settings() { factories = factories, shared = shared ? Settings.Shared.Allow : Settings.Shared.Deny, bespoke_keytypedict = bespoke_keytypedict };
2,033,480✔
185
            }
2,033,480✔
186
        }
187

188
        // This is used for passing data to the Parse and Compose functions.
189
        internal struct Settings
190
        {
191
            internal enum Shared
192
            {
193
                Deny,   // "this cannot be shared"
194
                Flexible,   // "this is an implicit child of something that had been requested to be shared; let it be shared, but don't warn if it can't be"
195
                Allow,  // "this thing has specifically been requested to be shared, spit out a warning if it can't be shared"
196
            }
197

198
            public Dictionary<Type, Func<Type, object>> factories;
199
            public Shared shared;
200

201
            public bool bespoke_keytypedict;
202

203
            public Settings CreateChild()
204
            {
247,171✔
205
                Settings rv = this;
247,171✔
206
                if (rv.shared == Shared.Allow)
247,171✔
207
                {
990✔
208
                    // Downgrade this in case we have something like a List<int>; we don't want to spit out warnings about int not being sharable
209
                    rv.shared = Shared.Flexible;
990✔
210
                }
990✔
211

212
                rv.bespoke_keytypedict = false;
247,171✔
213
                return rv;
247,171✔
214
            }
247,171✔
215

216
            internal IRecordable CreateRecordableFromFactory(Type type, string name, ReaderNode node)
217
            {
1,230✔
218
                // Iterate back to the appropriate type.
219
                Type targetType = type;
1,230✔
220
                Func<Type, object> maker = null;
1,230✔
221
                while (targetType != null)
1,470✔
222
                {
1,440✔
223
                    if (factories.TryGetValue(targetType, out maker))
1,440✔
224
                    {
1,200✔
225
                        break;
1,200✔
226
                    }
227

228
                    targetType = targetType.BaseType;
240✔
229
                }
240✔
230

231
                if (maker == null)
1,230✔
232
                {
30✔
233
                    return (IRecordable)type.CreateInstanceSafe(name, node);
30✔
234
                }
235
                else
236
                {
1,200✔
237
                    // want to propogate this throughout the factories list to save on time later
238
                    // we're actually doing the same BaseType thing again, starting from scratch
239
                    Type writeType = type;
1,200✔
240
                    while (writeType != targetType)
1,350✔
241
                    {
150✔
242
                        factories[writeType] = maker;
150✔
243
                        writeType = writeType.BaseType;
150✔
244
                    }
150✔
245

246
                    // oh right and I guess we should actually make the thing too
247
                    var obj = maker(type);
1,200✔
248

249
                    if (obj == null)
1,200✔
250
                    {
30✔
251
                        // fall back to default behavior
252
                        return (IRecordable)type.CreateInstanceSafe(name, node);
30✔
253
                    }
254
                    else if (!type.IsAssignableFrom(obj.GetType()))
1,170✔
255
                    {
120✔
256
                        Dbg.Err($"Custom factory generated {obj.GetType()} when {type} was expected; falling back on a default object");
120✔
257
                        return (IRecordable)type.CreateInstanceSafe(name, node);
120✔
258
                    }
259
                    else
260
                    {
1,050✔
261
                        // now that we've checked this is of the right type
262
                        return (IRecordable)obj;
1,050✔
263
                    }
264
                }
265
            }
1,230✔
266
        }
267

268
        public abstract IUserSettings UserSettings { get; }
269

270
        /// <summary>
271
        /// Provide the Context for this serialization step, which can be used to get diagnostic information about "where" it is.
272
        /// </summary>
273
        public abstract Context Context { get; }
274

275
        /// <summary>
276
        /// Serialize or deserialize a member of a class.
277
        /// </summary>
278
        /// <remarks>
279
        /// This function serializes or deserializes a class member. Call it with a reference to the member and a label for the member (usually the member's name.)
280
        ///
281
        /// In most cases, you don't need to do anything different for read vs. write; this function will figure out the details and do the right thing.
282
        ///
283
        /// Be aware that if you're reading an object that was serialized as .Shared(), the object may not be fully deserialized by the time this function returns.
284
        /// If you aren't completely certain that the object was unshared when serialized, do not access members of the recorded object - they may be in an unknown state.
285
        /// If you need to do something after all objects are fully deserialized, wait until Recorder.Read() is finished and do a post-processing pass.
286
        /// </remarks>
287
        public void Record<T>(ref T value, string label)
288
        {
426,685✔
289
            Record(ref value, label, new Parameters());
426,685✔
290
        }
426,685✔
291

292
        /// <summary>
293
        /// Serialize or deserialize a member of a class as if it were this class.
294
        /// </summary>
295
        /// <remarks>
296
        /// This function serializes or deserializes a class member as if it were this entire class. Call it with a reference to the member.
297
        ///
298
        /// This is intended for cases where a class's contents are a single method and where an extra level of indirection in XML files isn't desired.
299
        ///
300
        /// In most cases, you don't need to do anything different for read vs. write; this function will figure out the details and do the right thing.
301
        ///
302
        /// This does not work, at all, if any of the classes in the `this` chain are inherited; it needs to be able to fall back on default types.
303
        /// </remarks>
304
        public void RecordAsThis<T>(ref T value)
305
        {
870✔
306
            Record(ref value, "", new Parameters() { recorder = this, asThis = true });
870✔
307
        }
870✔
308

309
        internal abstract void Record<T>(ref T value, string label, Parameters parameters);
310

311
        /// <summary>
312
        /// Add a factory layer to objects created during this call.
313
        /// </summary>
314
        /// <remarks>
315
        /// This allows you to create your own object initializer for things deserialized during this call. Standard Recorder functionality will apply on the object returned.
316
        /// This is sometimes a convenient way to set per-object defaults when deserializing.
317
        ///
318
        /// The initializer layout takes the form of a dictionary from Type to Func&lt;Type, object&gt;.
319
        /// When creating a new object, Dec will first look for a dictionary key of that type, then continue checking base types iteratively until it either finds a callback or passes `object`.
320
        /// That callback will be given a desired type and must return either an object of that type, an object of a type derived from that type, or `null`.
321
        /// On `null`, Dec will fall back to its default behavior. In each other case, it will then be deserialized as usual.
322
        ///
323
        /// The factory callback will persist until the next Recorder is called; recursive calls past that will be reset to default behavior.
324
        /// This means that it will effectively tunnel through supported containers such as List&lt;&gt; and Dictionary&lt;&gt;, allowing you to control the constructor of `CustomType` in ` List&lt;CustomType&gt;`.
325
        ///
326
        /// Be aware that any classes created with a factory callback added *cannot* be referenced from multiple places in Record hierarchy - the normal ref structure does not function with them.
327
        /// Also, be aware that excessively deep hierarchies full of factory callbacks may result in performance issues when writing pretty-print XML; this is not likely to be a problem in normal code, however.
328
        /// For performance's sake, this function does not duplicate `factories` and may modify it for efficiency reasons.
329
        /// It can be reused, but should not be modified by the user once passed into a function once.
330
        ///
331
        /// This is incompatible with Shared().
332
        /// </remarks>
333
        public Parameters WithFactory(Dictionary<Type, Func<Type, object>> factories)
334
        {
2,280✔
335
            return new Parameters() { recorder = this, factories = factories };
2,280✔
336
        }
2,280✔
337

338
        /// <summary>
339
        /// Informs the recorder system that the next parameter will be a Dictionary&lt;Type, V&gt; where the key should be interpreted as the type of the V.
340
        /// </summary>
341
        /// <remarks>
342
        /// This is a specialty hack added for one specific user and may vanish with little warning, though if it does, it'll be replaced by something at least as capable.
343
        /// </remarks>
344
        public Recorder.Parameters Bespoke_KeyTypeDict()
345
        {
90✔
346
            return new Parameters() { recorder = this, bespoke_keytypedict = true };
90✔
347
        }
90✔
348

349
        /// <summary>
350
        /// Allow sharing for class objects referenced during this call.
351
        /// </summary>
352
        /// <remarks>
353
        /// Shared objects can be referenced from multiple classes. During serialization, these links will be stored; during deserialization, these links will be recreated.
354
        /// This is handy if (for example) your entities need to refer to each other for AI or targeting reasons.
355
        ///
356
        /// However, when reading, shared Recorder fields *must* be initially set to `null`.
357
        ///
358
        /// Dec objects are essentially treated as value types, and will be referenced appropriately even without this.
359
        ///
360
        /// This is incompatible with WithFactory().
361
        /// </remarks>
362
        public Parameters Shared()
363
        {
1,603,935✔
364
            return new Parameters() { recorder = this, shared = true };
1,603,935✔
365
        }
1,603,935✔
366

367
        /// <summary>
368
        /// Indicates that a field is intentionally unused and should be ignored.
369
        /// </summary>
370
        /// <remarks>
371
        /// Dec will output warnings if a field isn't being used, on the assumption that it's probably a mistake.
372
        ///
373
        /// Sometimes a field is ignored intentionally, usually for backwards compatibility reasons, and this function can be used to suppress that warning.
374
        /// </remarks>
375
        public virtual void Ignore(string label) { }
×
376

377
        /// <summary>
378
        /// Indicates whether this Recorder is being used for reading or writing.
379
        /// </summary>
380
        public enum Direction
381
        {
382
            Read,
383
            Write,
384
        }
385
        /// <summary>
386
        /// Indicates whether this Recorder is being used for reading or writing.
387
        /// </summary>
388
        public abstract Direction Mode { get; }
389
    }
390

391
    internal class RecorderWriter : Recorder
392
    {
393
        private bool asThis = false;
513,265✔
394
        private readonly HashSet<string> fields = new HashSet<string>();
513,265✔
395
        private readonly WriterNode node;
396

397
        internal RecorderWriter(WriterNode node)
513,265✔
398
        {
513,265✔
399
            this.node = node;
513,265✔
400
        }
513,265✔
401

402
        public override IUserSettings UserSettings { get => node.UserSettings; }
10✔
403

404
        public override Context Context { get => new Context(path: node.Path); }
130✔
405

406
        internal override void Record<T>(ref T value, string label, Parameters parameters)
407
        {
1,020,350✔
408
            if (asThis)
1,020,350✔
409
            {
50✔
410
                Dbg.Err($"Attempting to write a second field after a RecordAsThis call");
50✔
411
                return;
50✔
412
            }
413

414
            if (parameters.asThis)
1,020,300✔
415
            {
340✔
416
                if (fields.Count > 0)
340✔
417
                {
25✔
418
                    Dbg.Err($"Attempting to make a RecordAsThis call after writing a field");
25✔
419
                    return;
25✔
420
                }
421

422
                asThis = true;
315✔
423

424
                if (!node.FlagAsThis())
315✔
425
                {
20✔
426
                    // just give up and skip it
427
                    return;
20✔
428
                }
429

430
                if (node.AllowAsThis)
295✔
431
                {
265✔
432
                    Serialization.ComposeElement(node, value, typeof(T), asThis: true);
265✔
433

434
                    return;
265✔
435
                }
436
            }
30✔
437

438
            if (fields.Contains(label))
1,019,990✔
439
            {
25✔
440
                Dbg.Err($"Field `{label}` written multiple times");
25✔
441
                return;
25✔
442
            }
443

444
            fields.Add(label);
1,019,965✔
445

446
            Serialization.ComposeElement(node.CreateRecorderChild(label, parameters.CreateSettings()), value, typeof(T));
1,019,965✔
447
        }
1,020,350✔
448

449
        public override Direction Mode { get => Direction.Write; }
81,035✔
450
    }
451

452
    internal struct ReaderGlobals
453
    {
454
        public Dictionary<string, object> refs;
455
        public bool allowRefs;
456
        public bool allowReflection;
457
    }
458

459
    internal class RecorderReader : Recorder
460
    {
461
        private bool asThis = false;
510,345✔
462
        private readonly ReaderNode node;
463
        private ReaderGlobals readerGlobals;
464
        private bool disallowShared;
465
        private HashSet<string> seen;
466

467
        internal RecorderReader(ReaderNode node, ReaderGlobals globals, bool disallowShared = false, bool trackUsage = false)
510,345✔
468
        {
510,345✔
469
            this.node = node;
510,345✔
470
            this.readerGlobals = globals;
510,345✔
471
            this.disallowShared = disallowShared;
510,345✔
472

473
            if (trackUsage)
510,345✔
474
            {
389,375✔
475
                seen = new HashSet<string>();
389,375✔
476
            }
389,375✔
477
        }
510,345✔
478
        internal void AllowShared(ReaderGlobals newGlobals)
479
        {
145✔
480
            if (!disallowShared)
145✔
481
            {
×
482
                Dbg.Err($"{node.GetContext()}: Internal error, RecorderReader.AllowShared() called on a RecorderReader that does not disallow shared objects");
×
483
            }
×
484

485
            this.readerGlobals = newGlobals;
145✔
486
            disallowShared = false;
145✔
487
        }
145✔
488

489
        public override IUserSettings UserSettings { get => node.UserSettings; }
20✔
490

491
        public override Context Context { get => node.GetContext(); }
315✔
492

493
        internal override void Record<T>(ref T value, string label, Parameters parameters)
494
        {
1,013,905✔
495
            if (asThis)
1,013,905✔
496
            {
50✔
497
                Dbg.Err($"{node.GetContext()}: Attempting to read a second field after a RecordAsThis call");
50✔
498
                return;
50✔
499
            }
500

501
            if (parameters.asThis)
1,013,855✔
502
            {
545✔
503
                asThis = true;
545✔
504

505
                if (node.AllowAsThis && (node is ReaderNodeParseable nodeParseable))
545✔
506
                {
510✔
507
                    // Explicit cast here because we want an error if we have the wrong type!
508
                    value = (T)Serialization.ParseElement(new List<ReaderNodeParseable>() { nodeParseable }, typeof(T), value, readerGlobals, parameters.CreateSettings(), asThis: true);
510✔
509

510
                    return;
510✔
511
                }
512
            }
35✔
513

514
            if (disallowShared && parameters.shared)
1,013,345✔
515
            {
90✔
516
                Dbg.Err($"{node.GetContext()}: Shared object used in a context that disallows shared objects (probably ConverterFactory<>.Create())");
90✔
517
            }
90✔
518

519
            var recorded = node.GetChildNamed(label);
1,013,345✔
520
            if (recorded == null)
1,013,345✔
521
            {
340✔
522
                return;
340✔
523
            }
524

525
            seen?.Add(label);
1,013,005✔
526

527
            // Explicit cast here because we want an error if we have the wrong type!
528
            value = (T)recorded.ParseElement(typeof(T), value, readerGlobals, parameters.CreateSettings());
1,013,005✔
529
        }
1,013,905✔
530

531
        public override void Ignore(string label)
532
        {
5✔
533
            seen?.Add(label);
5✔
534
        }
5✔
535

536
        public override Direction Mode { get => Direction.Read; }
81,065✔
537

538
        internal void ReportUnusedFields()
539
        {
389,270✔
540
            if (seen == null)
389,270✔
541
            {
×
542
                Dbg.Err($"{node.GetContext()}: Internal error, RecorderReader.HasUnusedFields() called without trackUsage set");
×
543
                return;
×
544
            }
545

546
            if (asThis)
389,270✔
547
            {
510✔
548
                // field parsing deferred to our child anyway
549
                return;
510✔
550
            }
551

552
            var allChildren = node.GetAllChildren();
388,760✔
553
            if (seen.Count == allChildren.Length)
388,760✔
554
            {
388,750✔
555
                // we only register things that existed
556
                // so if "seen" is the same length as "all", then we've seen everything
557
                return;
388,750✔
558
            }
559

560
            var unused = new HashSet<string>(allChildren);
10✔
561
            unused.ExceptWith(seen);
10✔
562

563
            Dbg.Wrn($"{node.GetContext()}: Unused fields: {string.Join(", ", unused)}");
10✔
564
        }
389,270✔
565
    }
566
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc