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

SamboyCoding / Tomlet / 10360628975

12 Aug 2024 11:00PM UTC coverage: 91.979% (+0.03%) from 91.945%
10360628975

push

github

web-flow
Implement deserialization of dictionaries using primitives as keys (#44)

957 of 1106 branches covered (86.53%)

20 of 21 new or added lines in 2 files covered. (95.24%)

1 existing line in 1 file now uncovered.

1892 of 2057 relevant lines covered (91.98%)

220.44 hits per line

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

90.13
/Tomlet/TomlSerializationMethods.cs
1
using System;
2
using System.Collections;
3
using System.Collections.Generic;
4
using System.Globalization;
5
using System.Linq;
6
using System.Reflection;
7
using Tomlet.Attributes;
8
using Tomlet.Exceptions;
9
using Tomlet.Extensions;
10
using Tomlet.Models;
11

12
namespace Tomlet
13
{
14
    public static class TomlSerializationMethods
15
    {
16
        private static MethodInfo _stringKeyedDictionaryMethod = typeof(TomlSerializationMethods).GetMethod(nameof(StringKeyedDictionaryDeserializerFor), BindingFlags.Static | BindingFlags.NonPublic)!;
17
        private static MethodInfo _primitiveKeyedDictionaryMethod = typeof(TomlSerializationMethods).GetMethod(nameof(PrimitiveKeyedDictionaryDeserializerFor), BindingFlags.Static | BindingFlags.NonPublic)!;
18
        private static MethodInfo _genericDictionarySerializerMethod = typeof(TomlSerializationMethods).GetMethod(nameof(GenericDictionarySerializer), BindingFlags.Static | BindingFlags.NonPublic)!;
19
        private static MethodInfo _genericNullableSerializerMethod = typeof(TomlSerializationMethods).GetMethod(nameof(GenericNullableSerializer), BindingFlags.Static | BindingFlags.NonPublic)!;
20

21
        public delegate T Deserialize<out T>(TomlValue value);
22
        public delegate T ComplexDeserialize<out T>(TomlValue value, TomlSerializerOptions options);
23
        public delegate TomlValue? Serialize<in T>(T? t);
24
        public delegate TomlValue? ComplexSerialize<in T>(T? t, TomlSerializerOptions options);
25

26
        private static readonly Dictionary<Type, Delegate> Deserializers = new();
27
        private static readonly Dictionary<Type, Delegate> Serializers = new();
28

29

30
        [ExcludeFromCodeCoverage]
31
        static TomlSerializationMethods()
32
        {
33
            //Register built-in serializers
34

35
            //String
36
            Register(s => new TomlString(s!), value => (value as TomlString)?.Value ?? value.StringValue);
37

38
            //Bool
39
            Register(TomlBoolean.ValueOf, value => (value as TomlBoolean)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlBoolean), value.GetType(), typeof(bool)));
40

41
            //Byte
42
            Register(i => new TomlLong(i), value => (byte)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(byte))));
43

44
            //SByte
45
            Register(i => new TomlLong(i), value => (sbyte)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(sbyte))));
46

47
            //UShort
48
            Register(i => new TomlLong(i), value => (ushort)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(ushort))));
49

50
            //Short
51
            Register(i => new TomlLong(i), value => (short)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(short))));
52

53
            //UInt
54
            Register(i => new TomlLong(i), value => (uint)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(uint))));
55

56
            //Int
57
            Register(i => new TomlLong(i), value => (int)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(int))));
58

59
            //ULong
60
            Register(l => new TomlLong((long)l), value => (ulong)((value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(ulong))));
61

62
            //Long
63
            Register(l => new TomlLong(l), value => (value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(long)));
64

65
            //Double
66
            Register(d => new TomlDouble(d), value => (value as TomlDouble)?.Value ?? (value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlDouble), value.GetType(), typeof(double)));
67

68
            //Float
69
            Register(f => new TomlDouble(f), value => (float)((value as TomlDouble)?.Value ?? (value as TomlLong)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlDouble), value.GetType(), typeof(float))));
70

71
            //LocalDate(Time)
72
            Register(dt => dt.TimeOfDay == TimeSpan.Zero ? new TomlLocalDate(dt) : new TomlLocalDateTime(dt), value => (value as ITomlValueWithDateTime)?.Value ?? throw new TomlTypeMismatchException(typeof(ITomlValueWithDateTime), value.GetType(), typeof(DateTime)));
73

74
            //OffsetDateTime
75
            Register(odt => new TomlOffsetDateTime(odt), value => (value as TomlOffsetDateTime)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlOffsetDateTime), value.GetType(), typeof(DateTimeOffset)));
76

77
            //LocalTime
78
            Register(lt => new TomlLocalTime(lt), value => (value as TomlLocalTime)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLocalTime), value.GetType(), typeof(TimeSpan)));
79
        }
80

81
        /// <summary>
82
        /// Returns the default (reflection-based) serializer for the given type. Can be useful if you're implementing your own custom serializer but want to use the default behavior (e.g. to extend it or to use it as a fallback). 
83
        /// </summary>
84
        /// <param name="type">The type to get the default serializer for</param>
85
        /// <param name="options">The options to use for the serializer</param>
86
        /// <returns>The default reflection-based serializer for the given type.</returns>
87
        /// <exception cref="ArgumentException">Thrown if <paramref name="type"/> is a primitive type.</exception>
88
        public static Serialize<object> GetDefaultSerializerForType(Type type, TomlSerializerOptions? options = null)
89
        {
×
90
            options ??= TomlSerializerOptions.Default;
×
91
            if(type.IsPrimitive)
×
92
                throw new ArgumentException("Can't use reflection-based serializer for primitive types.", nameof(type));
×
93
            
94
            return TomlCompositeSerializer.For(type, options);
×
95
        }
×
96
        
97
        /// <summary>
98
        /// Returns the default (reflection-based) deserializer for the given type. Can be useful if you're implementing your own custom deserializer but want to use the default behavior (e.g. to extend it or to use it as a fallback).
99
        /// </summary>
100
        /// <param name="type">The type to get the default deserializer for</param>
101
        /// <param name="options">The options to use for the deserializer</param>
102
        /// <returns>The default reflection-based deserializer for the given type.</returns>
103
        /// <exception cref="ArgumentException">Thrown if <paramref name="type"/> is a primitive type.</exception>
104
        public static Deserialize<object> GetDefaultDeserializerForType(Type type, TomlSerializerOptions? options = null)
105
        {
×
106
            options ??= TomlSerializerOptions.Default;
×
107
            if(type.IsPrimitive)
×
108
                throw new ArgumentException("Can't use reflection-based deserializer for primitive types.", nameof(type));
×
109
            
110
            return TomlCompositeDeserializer.For(type, options);
×
111
        }
×
112

113
        internal static Serialize<object> GetSerializer(Type t, TomlSerializerOptions? options)
114
        {
258✔
115
            options ??= TomlSerializerOptions.Default;
258✔
116
            
117
            if (Serializers.TryGetValue(t, out var value))
258✔
118
                return (Serialize<object>)value;
169✔
119

120
            //First check, lists and arrays get serialized as enumerables.
121
            if (t.IsArray || t is { Namespace: "System.Collections.Generic", Name: "List`1" })
89✔
122
            {
10✔
123
                var arrSerializer = GenericEnumerableSerializer();
10✔
124
                Serializers[t] = arrSerializer;
10✔
125
                return arrSerializer;
10✔
126
            }
127

128
            //Check for dicts and nullables
129
            if (t.IsGenericType &&  t.GetGenericArguments() is { } genericArgs)
79✔
130
            {
17✔
131
                if (t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
17✔
132
                {
5✔
133
                    var serializer = _genericDictionarySerializerMethod.MakeGenericMethod(genericArgs);
5✔
134

135
                    var del = Delegate.CreateDelegate(typeof(ComplexSerialize<>).MakeGenericType(t), serializer);
5✔
136
                    var ret = (Serialize<object>)(dict => (TomlValue?)del.DynamicInvoke(dict, options));
10✔
137
                    Serializers[t] = ret;
5✔
138

139
                    return ret;
5✔
140
                }
141

142
                if (t.GetGenericTypeDefinition() == typeof(Nullable<>))
12✔
143
                {
1✔
144
                    var serializer = _genericNullableSerializerMethod.MakeGenericMethod(genericArgs);
1✔
145
                    
146
                    var del = Delegate.CreateDelegate(typeof(ComplexSerialize<>).MakeGenericType(t), serializer);
1✔
147
                    var ret = (Serialize<object>)(dict => (TomlValue?)del.DynamicInvoke(dict, options));
2✔
148
                    Serializers[t] = ret;
1✔
149
                    
150
                    return ret;
1✔
151
                }
152
            }
11✔
153
            
154
            //Now we've got dicts out of the way we can check if we're dealing with something that's IEnumerable and if so serialize as an array. We do this only *after* checking for dictionaries, because we don't want to serialize dictionaries as enumerables (i.e. table-arrays)
155
            if (typeof(IEnumerable).IsAssignableFrom(t))
73✔
156
            {
1✔
157
                var enumerableSerializer = GenericEnumerableSerializer();
1✔
158
                Serializers[t] = enumerableSerializer;
1✔
159
                return enumerableSerializer;
1✔
160
            }
161

162
            return TomlCompositeSerializer.For(t, options);
72✔
163
        }
258✔
164

165
        internal static Deserialize<object> GetDeserializer(Type t, TomlSerializerOptions? options)
166
        {
200✔
167
            options ??= TomlSerializerOptions.Default;
200✔
168
            
169
            if (Deserializers.TryGetValue(t, out var value))
200✔
170
                return (Deserialize<object>)value;
131✔
171

172
            //We allow deserializing to IEnumerable fields/props, by setting them an array. We DO NOT do anything for classes that implement IEnumerable, though, because that would mess with deserializing lists, dictionaries, etc.
173
            if (t.IsArray || t.IsInterface && typeof(IEnumerable).IsAssignableFrom(t)) 
69✔
174
            {
3✔
175
                var elementType = t.IsInterface ? t.GetGenericArguments()[0] : t.GetElementType()!;
3✔
176
                var arrayDeserializer = ArrayDeserializerFor(elementType, options);
3✔
177
                Deserializers[t] = arrayDeserializer;
3✔
178
                return arrayDeserializer;
3✔
179
            }
180

181
            if (t.Namespace == "System.Collections.Generic" && t.Name == "List`1")
66✔
182
            {
6✔
183
                var listDeserializer = ListDeserializerFor(t.GetGenericArguments()[0], options);
6✔
184
                Deserializers[t] = listDeserializer;
6✔
185
                return listDeserializer;
6✔
186
            }
187
            
188
            if(t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) && t.GetGenericArguments() is {Length: 1})
60!
189
            {
1✔
190
                var nullableDeserializer = NullableDeserializerFor(t, options);
1✔
191
                Deserializers[t] = nullableDeserializer;
1✔
192
                return nullableDeserializer;
1✔
193
            }
194

195
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>) && t.GetGenericArguments() is { Length: 2 } genericArgs)
59!
196
            {
5✔
197
                if (genericArgs[0] == typeof(string))
5✔
198
                {
1✔
199
                    return (Deserialize<object>)_stringKeyedDictionaryMethod.MakeGenericMethod(genericArgs[1]).Invoke(null, new object[]{options})!;
1✔
200
                }
201

202
                if (genericArgs[0].IsIntegerType() || genericArgs[0] == typeof(bool) || genericArgs[0] == typeof(char))
4!
203
                {
4✔
204
                    // float primitives not supported due to decimal point causing issues
205
                    return (Deserialize<object>)_primitiveKeyedDictionaryMethod.MakeGenericMethod(genericArgs).Invoke(null, new object[]{options})!;
4✔
206
                }
UNCOV
207
            }
×
208

209
            return TomlCompositeDeserializer.For(t, options);
54✔
210
        }
200✔
211

212
        private static Serialize<object?> GenericEnumerableSerializer() =>
213
            o =>
11✔
214
            {
22✔
215
                if (o is not IEnumerable arr)
22!
216
                    throw new Exception("How did ArraySerializer end up getting a non-array?");
×
217

11✔
218
                var ret = new TomlArray();
22✔
219
                foreach (var entry in arr)
176✔
220
                {
55✔
221
                    ret.Add(entry);
55✔
222
                }
55✔
223

11✔
224
                return ret;
22✔
225
            };
33✔
226

227
        private static Deserialize<object> ArrayDeserializerFor(Type elementType, TomlSerializerOptions options) =>
228
            value =>
3✔
229
            {
13✔
230
                if (value is not TomlArray tomlArray)
13!
231
                    throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), elementType.MakeArrayType());
×
232

3✔
233
                var ret = Array.CreateInstance(elementType, tomlArray.Count);
13✔
234
                var deserializer = GetDeserializer(elementType, options);
13✔
235
                for (var index = 0; index < tomlArray.ArrayValues.Count; index++)
88✔
236
                {
31✔
237
                    var arrayValue = tomlArray.ArrayValues[index];
31✔
238
                    ret.SetValue(deserializer(arrayValue), index);
31✔
239
                }
31✔
240

3✔
241
                return ret;
13✔
242
            };
16✔
243

244
        private static Deserialize<object> ListDeserializerFor(Type elementType, TomlSerializerOptions options)
245
        {
6✔
246
            var listType = typeof(List<>).MakeGenericType(elementType);
6✔
247
            var relevantAddMethod = listType.GetMethod("Add")!;
6✔
248

249
            return value =>
6✔
250
            {
10✔
251
                if (value is not TomlArray tomlArray)
10!
252
                    throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), listType);
×
253

6✔
254
                var ret = Activator.CreateInstance(listType)!;
10✔
255
                var deserializer = GetDeserializer(elementType, options);
10✔
256

6✔
257
                foreach (var arrayValue in tomlArray.ArrayValues)
78✔
258
                {
24✔
259
                    relevantAddMethod.Invoke(ret, new[] { deserializer(arrayValue) });
24✔
260
                }
24✔
261

6✔
262
                return ret;
10✔
263
            };
16✔
264
        }
6✔
265
        
266
        private static Deserialize<object> NullableDeserializerFor(Type nullableType, TomlSerializerOptions options)
267
        {
1✔
268
            var elementType = nullableType.GetGenericArguments()[0];
1✔
269
            var elementDeserializer = GetDeserializer(elementType, options);
1✔
270
            
271
            return value =>
1✔
272
            {
1✔
273
                //If we're deserializing, we know the value is not null
1✔
274
                var element = elementDeserializer(value);
1✔
275
                return Activator.CreateInstance(nullableType, element)!;
1✔
276
            };
2✔
277
        }
1✔
278

279
        private static Deserialize<Dictionary<string, T>> StringKeyedDictionaryDeserializerFor<T>(TomlSerializerOptions options)
280
        {
1✔
281
            var deserializer = GetDeserializer(typeof(T), options);
1✔
282

283
            return value =>
1✔
284
            {
1✔
285
                if (value is not TomlTable table)
1!
286
                    throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), typeof(Dictionary<string, T>));
×
287

1✔
288
                return table.Entries.ToDictionary(entry => entry.Key, entry => (T)deserializer(entry.Value));
5✔
289
            };
2✔
290
        }
1✔
291

292
        // unmanaged + IConvertible is the closest I can get to expressing "primitives only"
293
        private static Deserialize<Dictionary<TKey, TValue>> PrimitiveKeyedDictionaryDeserializerFor<TKey, TValue>(TomlSerializerOptions options) where TKey : unmanaged, IConvertible
294
        {
4✔
295
            var valueDeserializer = GetDeserializer(typeof(TValue), options);
4✔
296

297
            return value =>
4✔
298
            {
4✔
299
                if (value is not TomlTable table)
4!
NEW
300
                    throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), typeof(Dictionary<TKey, TValue>));
×
301

4✔
302
                return table.Entries.ToDictionary(
4✔
303
                    entry => (TKey)(entry.Key as IConvertible).ToType(typeof(TKey), CultureInfo.InvariantCulture),
12✔
304
                    entry => (TValue)valueDeserializer(entry.Value)
12✔
305
                );
4✔
306
            };
8✔
307
        }
4✔
308

309
        private static TomlValue? GenericNullableSerializer<T>(T? nullable, TomlSerializerOptions options) where T : struct
310
        {
1✔
311
            var elementSerializer = GetSerializer(typeof(T), options);
1✔
312
            
313
            if (nullable.HasValue)
1!
314
                return elementSerializer(nullable.Value);
1✔
315

316
            return null;
×
317
        }
1✔
318

319
        private static TomlValue GenericDictionarySerializer<TKey, TValue>(Dictionary<TKey, TValue> dict, TomlSerializerOptions options) where TKey : notnull
320
        {
5✔
321
            var valueSerializer = GetSerializer(typeof(TValue), options);
5✔
322

323
            var ret = new TomlTable();
5✔
324
            foreach (var entry in dict)
43✔
325
            {
14✔
326
                var keyAsString = entry.Key.ToString();
14✔
327
                
328
                if(keyAsString == null)
14✔
329
                    continue;
×
330

331
                var value = valueSerializer(entry.Value);
14✔
332
                
333
                if(value == null)
14✔
334
                    continue;
×
335
                
336
                ret.PutValue(keyAsString, value, true);
14✔
337
            }
14✔
338

339
            return ret;
5✔
340
        }
5✔
341

342
        internal static void Register<T>(Serialize<T>? serializer, Deserialize<T>? deserializer)
343
        {
15✔
344
            if (serializer != null)
15✔
345
            {
15✔
346
                RegisterSerializer(serializer);
15✔
347

348
                RegisterDictionarySerializer(serializer);
15✔
349
            }
15✔
350

351
            if (deserializer != null)
15✔
352
            {
15✔
353
                RegisterDeserializer(deserializer);
15✔
354
                RegisterDictionaryDeserializer(deserializer);
15✔
355
            }
15✔
356
        }
15✔
357

358
        internal static void Register(Type t, Serialize<object>? serializer, Deserialize<object>? deserializer)
359
        {
123✔
360
            if (serializer != null)
123✔
361
                RegisterSerializer(serializer);
71✔
362

363
            if (deserializer != null)
123✔
364
                RegisterDeserializer(deserializer);
52✔
365
        }
123✔
366

367
        private static void RegisterDeserializer<T>(Deserialize<T> deserializer)
368
        {
82✔
369
            object BoxedDeserializer(TomlValue value) => deserializer.Invoke(value) ?? throw new Exception($"TOML Deserializer returned null for type {nameof(T)}");
145!
370
            Deserializers[typeof(T)] = (Deserialize<object>)BoxedDeserializer;
82✔
371
        }
82✔
372

373
        private static void RegisterSerializer<T>(Serialize<T> serializer)
374
        {
101✔
375
            TomlValue? ObjectAcceptingSerializer(object value) => serializer.Invoke((T)value);
165✔
376
            Serializers[typeof(T)] = (Serialize<object>)ObjectAcceptingSerializer!;
101✔
377
        }
101✔
378

379
        private static void RegisterDictionarySerializer<T>(Serialize<T> serializer)
380
        {
15✔
381
            RegisterSerializer<Dictionary<string, T>>(dict =>
15✔
382
            {
6✔
383
                var table = new TomlTable();
6✔
384

15✔
385
                if (dict == null)
6!
386
                    return table;
×
387

15✔
388
                var keys = dict.Keys.ToList();
6✔
389
                var values = dict.Values.Select(serializer.Invoke).ToList();
6✔
390

15✔
391
                for (var i = 0; i < keys.Count; i++)
36✔
392
                {
12✔
393
                    table.PutValue(keys[i], values[i], true);
12✔
394
                }
12✔
395

15✔
396
                return table;
6✔
397
            });
21✔
398
        }
15✔
399

400
        private static void RegisterDictionaryDeserializer<T>(Deserialize<T> deserializer)
401
        {
15✔
402
            RegisterDeserializer(value =>
15✔
403
            {
3✔
404
                if (value is not TomlTable table)
3!
405
                    throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), typeof(Dictionary<string, T>));
×
406

15✔
407
                return table.Entries
3✔
408
                    .Select(kvp => new KeyValuePair<string, T>(kvp.Key, deserializer.Invoke(kvp.Value)))
6✔
409
                    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
15✔
410
            });
18✔
411
        }
15✔
412
    }
413
}
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