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

xoofx / Tomlyn / 6966056295

23 Nov 2023 05:47AM UTC coverage: 81.9% (-0.002%) from 81.902%
6966056295

push

github

xoofx
Fixes table arrays and inline (#72)

1994 of 2706 branches covered (0.0%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

4119 of 4758 relevant lines covered (86.57%)

1357.09 hits per line

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

96.43
/src/Tomlyn/Model/ModelToTomlTransform.cs
1
// Copyright (c) Alexandre Mutel. All rights reserved.
2
// Licensed under the BSD-Clause 2 license.
3
// See license.txt file in the project root for full license information.
4

5
using System;
6
using System.Collections.Generic;
7
using System.Globalization;
8
using System.IO;
9
using System.Text;
10
using System.Linq;
11
using Tomlyn.Helpers;
12
using Tomlyn.Model.Accessors;
13
using Tomlyn.Syntax;
14
using Tomlyn.Text;
15

16
namespace Tomlyn.Model;
17

18
internal class ModelToTomlTransform
19
{
20
    private readonly object _rootObject;
21
    private readonly DynamicModelWriteContext _context;
22
    private readonly TextWriter _writer;
23
    private readonly List<ObjectPath> _paths;
24
    private readonly List<ObjectPath> _currentPaths;
25
    private readonly Stack<List<KeyValuePair<string, object?>>> _tempPropertiesStack;
26
    private ITomlMetadataProvider? _metadataProvider;
27

28
    public ModelToTomlTransform(object rootObject, DynamicModelWriteContext context)
29
    {
30
        _rootObject = rootObject;
127✔
31
        _context = context;
127✔
32
        _writer = context.Writer;
127✔
33
        _paths = new List<ObjectPath>();
127✔
34
        _tempPropertiesStack = new Stack<List<KeyValuePair<string, object?>>>();
127✔
35
        _currentPaths = new List<ObjectPath>();
127✔
36
    }
127✔
37

38
    public void Run()
39
    {
40
        var itemAccessor = _context.GetAccessor(_rootObject.GetType());
127✔
41
        if (itemAccessor is ObjectDynamicAccessor objectDynamicAccessor)
127!
42
        {
43
            VisitObject(objectDynamicAccessor, _rootObject, false);
127✔
44
        }
45
        else
46
        {
47
            _context.Diagnostics.Error(new SourceSpan(), $"The root object must a class with properties or a dictionary. Cannot be of kind {itemAccessor}.");
×
48
        }
49
    }
×
50

51
    private void PushName(string name, bool isTableArray)
52
    {
53
        _paths.Add(new ObjectPath(name, isTableArray));
187✔
54
    }
187✔
55

56
    private void WriteHeaderTable()
57
    {
58
        var name = _paths[_paths.Count - 1].Name;
93✔
59
        WriteLeadingTrivia(name);
93✔
60
        _writer.Write("[");
93✔
61
        WriteDottedKeys();
93✔
62
        _writer.Write("]");
93✔
63
        WriteTrailingTrivia(name);
93✔
64
        _writer.WriteLine();
93✔
65
        WriteTrailingTriviaAfterEndOfLine(name);
93✔
66
        _currentPaths.Clear();
93✔
67
        _currentPaths.AddRange(_paths);
93✔
68
    }
93✔
69

70
    private void WriteHeaderTableArray()
71
    {
72
        var name = _paths[_paths.Count - 1].Name;
57✔
73
        WriteLeadingTrivia(name);
57✔
74
        _writer.Write("[[");
57✔
75
        WriteDottedKeys();
57✔
76
        _writer.Write("]]");
57✔
77
        WriteTrailingTrivia(name);
57✔
78
        _writer.WriteLine();
57✔
79
        WriteTrailingTriviaAfterEndOfLine(name);
57✔
80
        _currentPaths.Clear();
57✔
81
        _currentPaths.AddRange(_paths);
57✔
82
    }
57✔
83

84
    private void WriteDottedKeys()
85
    {
86
        bool isFirst = true;
150✔
87
        foreach (var name in _paths)
798✔
88
        {
89
            if (!isFirst)
249✔
90
            {
91
                _writer.Write(".");
99✔
92
            }
93

94
            WriteKey(name.Name);
249✔
95
            isFirst = false;
249✔
96
        }
97
    }
150✔
98

99
    private void WriteKey(string name)
100
    {
101
        _writer.Write(EscapeKey(name));
846✔
102
    }
846✔
103

104
    private string EscapeKey(string name)
105
    {
106
        if (string.IsNullOrWhiteSpace(name)) return $"\"{name.EscapeForToml()}\"";
848✔
107
        
108
        // A-Za-z0-9_-
109
        foreach (var c in name)
11,646✔
110
        {
111
            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') || c == '.')
4,991✔
112
            {
113
                return $"\"{name.EscapeForToml()}\"";
24✔
114
            }
115
        }
116

117
        return name;
820✔
118
    }
119

120
    private void EnsureScope()
121
    {
122
        if (IsCurrentScopeValid()) return;
1,195✔
123

124
        if (_paths.Count == 0)
93!
125
        {
126
            _currentPaths.Clear();
×
127
        }
128
        else
129
        {
130
            var lastObjectPath = _paths[_paths.Count - 1];
93✔
131

132
            if (lastObjectPath.IsTableArray)
93!
133
            {
134
                WriteHeaderTableArray();
×
135
            }
136
            else
137
            {
138
                WriteHeaderTable();
93✔
139
            }
140
        }
141
    }
93✔
142

143
    private bool IsCurrentScopeValid()
144
    {
145
        if (_paths.Count == _currentPaths.Count)
644✔
146
        {
147
            for (var index = 0; index < _paths.Count; index++)
1,536✔
148
            {
149
                var path1 = _paths[index];
217✔
150
                var path2 = _currentPaths[index];
217✔
151
                if (!path1.Equals(path2)) return false;
243✔
152
            }
153

154
            return true;
551✔
155
        }
156

157
        return false;
67✔
158
    }
159

160

161
    private void PopName()
162
    {
163
        _paths.RemoveAt(_paths.Count - 1);
187✔
164
    }
187✔
165

166
    private bool VisitObject(ObjectDynamicAccessor accessor, object currentObject, bool inline)
167
    {
168
        bool hasElements = false;
356✔
169
        var isFirst = true;
356✔
170

171
        var previousMetadata = _metadataProvider;
356✔
172
        _metadataProvider = currentObject as ITomlMetadataProvider;
356✔
173
        var properties = _tempPropertiesStack.Count > 0 ? _tempPropertiesStack.Pop() : new List<KeyValuePair<string, object?>>();
356✔
174
        try
175
        {
176

177

178
            // Pre-convert values to TOML values
179
            var convertToToml = _context.ConvertToToml;
356✔
180
            if (convertToToml != null)
356✔
181
            {
182
                foreach (var property in accessor.GetProperties(currentObject))
4✔
183
                {
184
                    // Allow to convert a value to a TOML simpler value before serializing.
185
                    var value = property.Value;
1✔
186
                    if (value is not null)
1✔
187
                    {
188
                        var result = convertToToml(value);
1✔
189
                        if (result != null)
1✔
190
                        {
191
                            value = result;
1✔
192
                        }
193
                        properties.Add(new KeyValuePair<string, object?>(property.Key, value));
1✔
194
                    }
195
                }
196
            }
197
            else
198
            {
199
                properties.AddRange(accessor.GetProperties(currentObject));
355✔
200
            }
201

202
            // Sort primitive first
203
            properties = properties.OrderBy(_ => _,
1,180✔
204
            Comparer<KeyValuePair<string,object>>.Create((left, right) =>
356✔
205
            {
356✔
206
                var leftValue = left.Value;
565✔
207
                var rightValue = right.Value;
565✔
208
                if (leftValue is null) return rightValue is null ? 0 : -1;
593!
209
                if (rightValue is null) return 1;
547✔
210

356✔
211
                var leftAccessor = _context.GetAccessor(leftValue.GetType());
527✔
212
                var rightAccessor = _context.GetAccessor(rightValue.GetType());
527✔
213
                if (leftAccessor.Kind == ReflectionObjectKind.Primitive)
527✔
214
                {
356✔
215
                    return (rightAccessor.Kind == ReflectionObjectKind.Primitive) ? 0 : -1;
435✔
216
                }
356✔
217
                else if (rightAccessor.Kind == ReflectionObjectKind.Primitive)
92✔
218
                {
356✔
219
                    return 1;
16✔
220
                }
356✔
221

356✔
222
                // Otherwise don't change the order if we don't have primitives
356✔
223
                return 0;
76✔
224
            })).ToList();
356✔
225

226
            // Probe inline for each key
227
            // If we require a key to be inlined, inline the rest
228
            // unless for the last key, if it doesn't need to be inline, we keep it as it is
229
            bool propInline = inline;
356✔
230

231
            object? lastValue = null;
356✔
232
            bool lastInline = false;
356✔
233
            if (!inline)
356✔
234
            {
235
                foreach (var prop in properties)
2,308✔
236
                {
237
                    lastValue = prop.Value;
811✔
238
                    bool isRequiringInline = IsRequiringInline(prop.Value);
811✔
239
                    lastInline = isRequiringInline;
811✔
240
                    if (isRequiringInline)
811✔
241
                    {
242
                        propInline = true;
45✔
243
                    }
244
                }
245
            }
246

247
            foreach (var prop in properties)
2,360✔
248
            {
249
                // Skip any null properties
250
                if (prop.Value is null) continue;
824✔
251
                var name = prop.Key;
784✔
252

253
                if (inline && !isFirst)
784✔
254
                {
255
                    _writer.Write(", ");
2✔
256
                }
257

258
                bool isLastValue = lastValue is not null && ReferenceEquals(lastValue, prop.Value);
784✔
259
                var propToInline = (!isLastValue || lastInline) && propInline;
784✔
260

261
                // If we switch from non inline to inline, ensure that the scope is here
262
                if (!inline && propToInline)
784✔
263
                {
264
                    EnsureScope();
77✔
265
                }
266

267
                if (!inline)
784✔
268
                {
269
                    WriteLeadingTrivia(name);
771✔
270
                }
271

272
                var valueAccessor = WriteKeyValue(name, prop.Value, propToInline);
784✔
273

274
                // Special case to not output duplicated new lines that were already handled
275
                // WriteKeyValue
276
                if (!inline && (valueAccessor is PrimitiveDynamicAccessor || propToInline))
784✔
277
                {
278
                    WriteTrailingTrivia(name);
584✔
279
                    _writer.WriteLine();
584✔
280
                    WriteTrailingTriviaAfterEndOfLine(name);
584✔
281
                }
282

283
                hasElements = true;
784✔
284
                isFirst = false;
784✔
285
            }
286
        }
287
        finally
288
        {
289
            _metadataProvider = previousMetadata;
356✔
290
            properties.Clear();
356✔
291
            _tempPropertiesStack.Push(properties);
356✔
292
        }
356✔
293

294
        return hasElements;
356✔
295
    }
296

297
    private void VisitList(ListDynamicAccessor accessor, object currentObject, bool inline)
298
    {
299
        bool isFirst = true;
100✔
300
        foreach (var value in accessor.GetElements(currentObject))
596✔
301
        {
302
            // Skip any null value
303
            if (value is null) continue; // TODO: should emit an error?
198✔
304

305
            var itemAccessor = _context.GetAccessor(value.GetType());
198✔
306

307
            if (inline)
198✔
308
            {
309
                if (!isFirst)
141✔
310
                {
311
                    _writer.Write(", ");
70✔
312
                }
313

314
                WriteValueInline(itemAccessor, value);
141✔
315
                isFirst = false;
141✔
316
            }
317
            else
318
            {
319
                var previousMetadata = _metadataProvider;
57✔
320
                try
321
                {
322
                    _metadataProvider = value as ITomlMetadataProvider;
57✔
323
                    WriteHeaderTableArray();
57✔
324
                }
57✔
325
                finally
326
                {
327
                    _metadataProvider = previousMetadata;
57✔
328
                }
57✔
329
                VisitObject((ObjectDynamicAccessor)itemAccessor, value, false);
57✔
330
            }
331
        }
332
    }
100✔
333

334
    private DynamicAccessor WriteKeyValue(string name, object value, bool inline)
335
    {
336
        var accessor = _context.GetAccessor(value.GetType());
784!
337

338
        switch (accessor)
339
        {
340
            case ListDynamicAccessor listDynamicAccessor:
341
            {
342
                bool wasInline = inline;
75✔
343
                // Switch to inline if the object is a primitive
344
                if (!inline)
75✔
345
                {
346
                    inline = IsRequiringInline(listDynamicAccessor, value, 1);
28✔
347
                }
348

349
                if (inline)
75✔
350
                {
351
                    if (!wasInline) EnsureScope();
47!
352
                    WriteKey(name);
47✔
353
                    _writer.Write(" = [");
47✔
354
                    VisitList(listDynamicAccessor, value, true);
47✔
355
                    _writer.Write("]");
47✔
356
                }
357
                else
358
                {
359
                    PushName(name, true);
28✔
360
                    VisitList(listDynamicAccessor, value, false);
28✔
361
                    PopName();
28✔
362
                }
363
            }
364
                break;
28✔
365
            case ObjectDynamicAccessor objectAccessor:
366
                if (inline)
165✔
367
                {
368
                    WriteKey(name);
6✔
369
                    _writer.Write(" = {");
6✔
370
                    VisitObject(objectAccessor, value, true);
6✔
371
                    _writer.Write("}");
6✔
372
                }
373
                else
374
                {
375
                    PushName(name, false);
159✔
376
                    var hasElements = VisitObject(objectAccessor, value, false);
159✔
377
                    if (!hasElements)
159✔
378
                    {
379
                        var previousMetadataProvider = _metadataProvider;
23✔
380
                        _metadataProvider = value as ITomlMetadataProvider;
23✔
381
                        try
382
                        {
383
                            // Force to have a scope to create the object
384
                            EnsureScope();
23✔
385
                        }
23✔
386
                        finally
387
                        {
388
                            _metadataProvider = previousMetadataProvider;
23✔
389
                        }
23✔
390
                    }
391
                    PopName();
159✔
392
                }
393
                break;
159✔
394
            case PrimitiveDynamicAccessor primitiveDynamicAccessor:
395
                EnsureScope();
544✔
396
                WriteKey(name);
544✔
397
                _writer.Write(" = ");
544✔
398
                WritePrimitive(value, GetDisplayKind(name));
544✔
399
                break;
544✔
400
            default:
401
                throw new ArgumentOutOfRangeException(nameof(accessor));
×
402
        }
403

404
        return accessor;
784✔
405
    }
406

407
    private TomlPropertyDisplayKind GetDisplayKind(string name)
408
    {
409
        var kind = TomlPropertyDisplayKind.Default;
544✔
410
        if (_metadataProvider is not null && _metadataProvider.PropertiesMetadata is not null && _metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata))
544✔
411
        {
412
            kind = propertyMetadata.DisplayKind;
173✔
413
        }
414

415
        return kind;
544✔
416
    }
417

418
    private bool IsRequiringInline(object? value)
419
    {
420
        if (value is null) return false;
851✔
421

422
        var accessor = _context.GetAccessor(value.GetType());
771✔
423

424
        switch (accessor)
425
        {
426
            case ListDynamicAccessor listDynamicAccessor:
427
                    return IsRequiringInline(listDynamicAccessor, value, 1);
74✔
428
            default:
429
                return false;
697✔
430
        }
431
    }
432

433
    private bool IsRequiringInline(ListDynamicAccessor accessor, object value, int parentConsecutiveList)
434
    {
435
        // Always disable inline for TomlTableArray
436
        // This is only working for default TomlTableArray model
437
        if (value is TomlTableArray) return false;
149✔
438

439
        foreach (var element in accessor.GetElements(value))
266✔
440
        {
441
            if (element is null) continue; // TODO: should this log an error?
88✔
442
            var elementAccessor = _context.GetAccessor(element.GetType());
88✔
443

444
            if (elementAccessor is PrimitiveDynamicAccessor) return true;
129✔
445

446
            if (elementAccessor is ListDynamicAccessor listDynamicAccessor)
47✔
447
            {
448
                return IsRequiringInline(listDynamicAccessor, element, parentConsecutiveList + 1);
17✔
449

450
            }
451
            else if (elementAccessor is ObjectDynamicAccessor objAccessor)
30✔
452
            {
453
                // Case of an array-of-array of table
454
                if (parentConsecutiveList > 1) return true;
33✔
455
                return IsRequiringInline(objAccessor, element);
27✔
456
            }
457
        }
458

459
        // Specially for empty list
460
        return parentConsecutiveList > 1;
1✔
461
    }
88✔
462

463
    private bool IsRequiringInline(ObjectDynamicAccessor accessor, object value)
464
    {
465
        foreach (var prop in accessor.GetProperties(value))
157✔
466
        {
467
            var propValue = prop.Value;
50✔
468
            if (propValue is null)
50✔
469
            {
470
                continue;
471
            }
472

473
            var propValueAccessor = _context.GetAccessor(propValue.GetType());
40✔
474
            if (propValueAccessor is ListDynamicAccessor listDynamicAccessor)
40!
475
            {
UNCOV
476
                return IsRequiringInline(listDynamicAccessor, propValue, 1);
×
477

478
            }
479
            else if (propValueAccessor is ObjectDynamicAccessor objAccessor)
40✔
480
            {
481
                return IsRequiringInline(objAccessor, propValue);
3✔
482
            }
483
        }
484

485
        return false;
27✔
486
    }
3✔
487

488
    private void WriteLeadingTrivia(string name)
489
    {
490
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.LeadingTrivia is null) return;
1,829✔
491

492
        foreach (var trivia in propertyMetadata.LeadingTrivia)
126✔
493
        {
494
            if (trivia.Text is not null) _writer.Write(trivia.Text);
100✔
495
        }
496
    }
13✔
497

498
    private void WriteTrailingTrivia(string name)
499
    {
500
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.TrailingTrivia is null) return;
1,427✔
501

502
        foreach (var trivia in propertyMetadata.TrailingTrivia)
194✔
503
        {
504
            if (trivia.Text is not null) _writer.Write(trivia.Text);
112✔
505
        }
506
    }
41✔
507

508
    private void WriteTrailingTriviaAfterEndOfLine(string name)
509
    {
510
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.TrailingTriviaAfterEndOfLine is null) return;
1,364✔
511

512
        foreach (var trivia in propertyMetadata.TrailingTriviaAfterEndOfLine)
516✔
513
        {
514
            if (trivia.Text is not null) _writer.Write(trivia.Text);
308✔
515
        }
516
    }
104✔
517

518
    private void WriteValueInline(DynamicAccessor accessor, object? value)
519
    {
520
        if (value is null) return;
141!
521

522
        switch (accessor)
523
        {
524
            case ListDynamicAccessor listDynamicAccessor:
525
                _writer.Write("[");
25✔
526
                    VisitList(listDynamicAccessor, value, true);
25✔
527
                _writer.Write("]");
25✔
528
                break;
25✔
529
            case ObjectDynamicAccessor objectAccessor:
530
                _writer.Write("{");
7✔
531
                VisitObject(objectAccessor, value, true);
7✔
532
                _writer.Write("}");
7✔
533
                break;
7✔
534
            case PrimitiveDynamicAccessor primitiveDynamicAccessor:
535
                WritePrimitive(value, TomlPropertyDisplayKind.Default);
109✔
536
                break;
109✔
537
            default:
538
                throw new ArgumentOutOfRangeException(nameof(accessor));
×
539
        }
540
    }
541

542

543
    private void WritePrimitive(object primitive, TomlPropertyDisplayKind displayKind)
544
    {
545
        if (primitive is bool b)
653✔
546
        {
547
            _writer.Write(TomlFormatHelper.ToString(b));
30✔
548
        }
549
        else if (primitive is string s)
623✔
550
        {
551
            _writer.Write(TomlFormatHelper.ToString(s, displayKind));
258✔
552
        }
553
        else if (primitive is int i32)
365✔
554
        {
555
            _writer.Write(TomlFormatHelper.ToString(i32, displayKind));
27✔
556
        }
557
        else if (primitive is long i64)
338✔
558
        {
559
            _writer.Write(TomlFormatHelper.ToString(i64, displayKind));
164✔
560
        }
561
        else if (primitive is uint u32)
174✔
562
        {
563
            _writer.Write(TomlFormatHelper.ToString(u32, displayKind));
8✔
564
        }
565
        else if (primitive is ulong u64)
166✔
566
        {
567
            _writer.Write(TomlFormatHelper.ToString(u64, displayKind));
8✔
568
        }
569
        else if (primitive is sbyte i8)
158✔
570
        {
571
            _writer.Write(TomlFormatHelper.ToString(i8, displayKind));
8✔
572
        }
573
        else if (primitive is byte u8)
150✔
574
        {
575
            _writer.Write(TomlFormatHelper.ToString(u8, displayKind));
8✔
576
        }
577
        else if (primitive is short i16)
142✔
578
        {
579
            _writer.Write(TomlFormatHelper.ToString(i16, displayKind));
8✔
580
        }
581
        else if (primitive is ushort u16)
134✔
582
        {
583
            _writer.Write(TomlFormatHelper.ToString(u16, displayKind));
8✔
584
        }
585
        else if (primitive is float f32)
126✔
586
        {
587
            _writer.Write(TomlFormatHelper.ToString(f32));
8✔
588
        }
589
        else if (primitive is double f64)
118✔
590
        {
591
            _writer.Write(TomlFormatHelper.ToString(f64));
53✔
592
        }
593
        else if (primitive is TomlDateTime tomlDateTime)
65✔
594
        {
595
            _writer.Write(TomlFormatHelper.ToString(tomlDateTime));
32✔
596
        }
597
        else if (primitive is DateTime dateTime)
33✔
598
        {
599
            _writer.Write(TomlFormatHelper.ToString(dateTime, displayKind));
8✔
600
        }
601
        else if (primitive is DateTimeOffset dateTimeOffset)
25✔
602
        {
603
            _writer.Write(TomlFormatHelper.ToString(dateTimeOffset, displayKind));
8✔
604
        }
605
        else if (primitive is Enum enumValue)
17✔
606
        {
607
            _writer.Write(TomlFormatHelper.ToString(enumValue.ToString(), displayKind));
1✔
608
        }
609
#if NET6_0_OR_GREATER
610
        else if (primitive is DateOnly dateOnly)
16✔
611
        {
612
            _writer.Write(TomlFormatHelper.ToString(dateOnly, displayKind));
8✔
613
        }
614
        else if (primitive is TimeOnly timeOnly)
8!
615
        {
616
            _writer.Write(TomlFormatHelper.ToString(timeOnly, displayKind));
8✔
617
        }
618
#endif
619
        else
620
        {
621
            // Unexpected
622
            throw new InvalidOperationException($"Invalid primitive {primitive.GetType().FullName}");
×
623
        }
624
    }
625

626
    private record struct ObjectPath(string Name, bool IsTableArray);
627
}
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