GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALConverter.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.IO;
20using System.Linq;
21using System.Text;
22using System.Threading.Tasks;
23using System.Xml;
24using System.Xml.Serialization;
25using static GenerallyPositive.Enums;
26using YamlDotNet.Serialization;
27using YamlDotNet.Serialization.NamingConventions;
28using YamlDotNet.Serialization.ObjectGraphTraversalStrategies;
29using YamlDotNet.Serialization.ObjectFactories;
30using YamlDotNet.Core;
31using YamlDotNet.Core.Events;
32using System.Reflection;
33using Newtonsoft.Json;
34using System.Collections;
35using System.ComponentModel;
36using System.Text.RegularExpressions;
37
38namespace GenerallyPositive
39{
40 public class GPALConverter : IGPALConverter
41 {
42 private ConverterSettings _converterSettings = null;
43
47 internal ConverterSettings ConverterSettings
48 {
49 get
50 {
51 return _converterSettings;
52 }
53
54 set
55 {
56 _converterSettings = value;
57 }
58 }
59 private System.Threading.Timer _idleTimer;
60 private readonly object _timerLock = new object();
61 private const int IdleTimeoutMs = 5000;
62
63 internal GPALConverter()
64 {
65 ConverterSettings = new ConverterSettings();
66 }
67
68 private void ResetIdleTimer()
69 {
70 lock (_timerLock)
71 {
72 if (_idleTimer == null)
73 _idleTimer = new System.Threading.Timer(_ => { lock (_timerLock) { ConverterSettings.ClearDerived(); } }, null, IdleTimeoutMs, System.Threading.Timeout.Infinite);
74 else
75 _idleTimer.Change(IdleTimeoutMs, System.Threading.Timeout.Infinite);
76 }
77 }
78
79 private void CancelIdleTimer()
80 {
81 lock (_timerLock)
82 {
83 _idleTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
84 }
85 }
86
87 #region Fluent Interface
93 {
94 return this;
95 }
96
101 public IAllowConverterSettingsAndActions WithInput(List<Dictionary<object, dynamic>> inputDictionary)
102 {
103 CancelIdleTimer();
104 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
105 ConverterSettings.ClearDerived();
106 ConverterSettings.InputClass = null;
107 ConverterSettings.InputClassType = null;
108 ConverterSettings.InputClassElementType = null;
109 ConverterSettings.InputFirstLineIsColumnHeaders = false;
110 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
111 ConverterSettings.InputDictionary = inputDictionary;
112 ConverterSettings.InDataFormat.Add(DataFormat.DICTIONARY);
113
114 return this;
115 }
116
122 {
123 CancelIdleTimer();
124 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
125 ConverterSettings.ClearDerived();
126 ConverterSettings.InputClass = null;
127 ConverterSettings.InputClassType = null;
128 ConverterSettings.InputClassElementType = null;
129 ConverterSettings.InputFirstLineIsColumnHeaders = false;
130 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
131 ConverterSettings.InputDatabase = inputDatabase;
132 ConverterSettings.InDataFormat.Add(DataFormat.DATABASE);
133
134 return this;
135 }
136
142 {
143 ConverterSettings.OutputFile = null;
144 ConverterSettings.OutputClass = null;
145 ConverterSettings.OutputClassType = null;
146 ConverterSettings.OutputClassElementType = null;
147 ConverterSettings.OutputGrid = null;
148
149 ConverterSettings.OutputDatabase = outputDatabase;
150 ConverterSettings.OutDataFormat = DataFormat.DATABASE;
151 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{ConverterSettings.InDataFormat.FirstOrDefault()}] data to database.", this, GPALObjectType.Converter);
152 return Convert(ref outputDatabase);
153 }
154
159 public IAllowConverterSettingsAndActions WithInput(IGPALGrid<string> inputGrid)
160 {
161 CancelIdleTimer();
162 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
163 ConverterSettings.ClearDerived();
164 ConverterSettings.InputClass = null;
165 ConverterSettings.InputClassType = null;
166 ConverterSettings.InputClassElementType = null;
167 ConverterSettings.InputFirstLineIsColumnHeaders = false;
168 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
169 // Clear the other input sources so Convert()'s dispatch (InputFile, then InputDatabase, then
170 // InputGrid) actually reaches the grid branch - otherwise a prior file/database input would be
171 // reprocessed. Mirrors WithInput(dynamic), which clears InputFile for the same reason.
172 ConverterSettings.InputFile = null;
173 ConverterSettings.InputDatabase = null;
174 ConverterSettings.InputGrid.Add(inputGrid);
175 ConverterSettings.InDataFormat.Add(DataFormat.GRID);
176 ConverterSettings.InDelimiter = ',';
177
178 return this;
179 }
180
185 public IAllowConverterInputAndActions SaveTo(ref IGPALGrid<string> outputGrid)
186 {
187 ConverterSettings.OutputDatabase = null;
188 ConverterSettings.OutputFile = null;
189 ConverterSettings.OutputClassType = outputGrid.GetType();
190 ConverterSettings.OutputClassElementType = ConverterSettings.OutputClassType.GetElementType();
191 if (null == ConverterSettings.OutputClassElementType && 0 < ConverterSettings.OutputClassType.GetGenericArguments().Length)
192 if (true == ConverterHelper.IsDictionaryType(ConverterSettings.OutputClassType))
193 ConverterSettings.OutputClassElementType = ConverterSettings.OutputClassType.GetGenericArguments()[1];
194 else
195 ConverterSettings.OutputClassElementType = ConverterSettings.OutputClassType.GetGenericArguments()[0];
196 ConverterSettings.OutputGrid = outputGrid;
197 ConverterSettings.OutDataFormat = DataFormat.GRID;
198 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{ConverterSettings.InDataFormat.FirstOrDefault()}] data to grid.", this, GPALObjectType.Converter);
199
200 Convert(ref outputGrid);
201
202 return this;
203 }
204
220 {
221 CancelIdleTimer();
222 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
223 ConverterSettings.ClearDerived();
224 ConverterSettings.InputClass = null;
225 ConverterSettings.InputClassType = null;
226 ConverterSettings.InputClassElementType = null;
227 ConverterSettings.InputFirstLineIsColumnHeaders = false;
228 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
229 ConverterSettings.InputData.Add(inputData);
230 if (null != ConverterSettings.InDelimiter && true == ConverterSettings.InDelimiter.HasValue)
231 ConverterSettings.InDataFormat.Add(GetDataFormat(ConverterSettings.InDelimiter.Value));
232 else
233 ConverterSettings.InDataFormat.Add(DataFormatDetector.DetectFormat(inputData));
234
235 return this;
236 }
237
244 {
245 CancelIdleTimer();
246 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
247 ConverterSettings.ClearDerived();
248 ConverterSettings.InputClass = null;
249 ConverterSettings.InputClassType = null;
250 ConverterSettings.InputClassElementType = null;
251 ConverterSettings.InputFirstLineIsColumnHeaders = false;
252 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
253 ConverterSettings.InputFile = inputFile;
254 foreach (string filename in inputFile.Filenames)
255 ConverterSettings.InDataFormat.Add(ConverterHelper.GetDataFormatFromExtension(filename));
256 return this;
257 }
258
264 {
265 ConverterSettings.OutputFile = outputFile;
266 ConverterSettings.OutDataFormat = ConverterHelper.GetDataFormatFromExtension(outputFile.Filename); // will iterate files in convert and determine each dataformat
267 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Appending [{ConverterSettings.InDataFormat.FirstOrDefault()}] data as [{ConverterSettings.OutDataFormat}] to [{outputFile.Filename}].", this, GPALObjectType.Converter);
268 return Convert(ref outputFile, true);
269 }
270
283 {
284 ConverterSettings.OutputFile = outputFile;
285 ConverterSettings.OutDataFormat = ConverterHelper.GetDataFormatFromExtension(outputFile.Filename); // will iterate files in convert and determine each dataformat
286 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{ConverterSettings.InDataFormat.FirstOrDefault()}] data as [{ConverterSettings.OutDataFormat}] to [{outputFile.Filename}].", this, GPALObjectType.Converter);
287 Convert(ref outputFile);
288 outputFile.ReturnFilenames.Add(outputFile.Filename); // return the filename we actually save to as a filename
289 return this;
290 }
291
298 {
299 // a grid held as IAllowGridActions binds here rather than to the grid overload, and taken as an
300 // object it serializes as the rows of cells it enumerates: a list of lists, with the column names
301 // never read. It is the same grid either way, so it goes where a grid goes
302 if (inputClass is IGPALGrid<string> handedOverGrid)
303 return WithInput(handedOverGrid);
304
305 CancelIdleTimer();
306 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
307 ConverterSettings.ClearDerived();
308 ConverterSettings.InputFirstLineIsColumnHeaders = false;
309 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
310 ConverterSettings.InputFile = null;
311 ConverterSettings.InputClass = inputClass;
312 ConverterSettings.InputClassType = inputClass.GetType();
313 ConverterSettings.InputClassElementType = ConverterSettings.InputClassType.GetElementType();
314 if (null == ConverterSettings.InputClassElementType && ConverterSettings.InputClassType.IsGenericType)
315 {
316 var genericArgs = ConverterSettings.InputClassType.GetGenericArguments();
317 ConverterSettings.InputClassElementType = ConverterHelper.IsDictionaryType(ConverterSettings.InputClassType) && genericArgs.Length > 1
318 ? genericArgs[1] // Use value type for dictionaries (e.g., SelectorSet)
319 : genericArgs.FirstOrDefault();
320 }
321 HashSet<object> visited = new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
322
323 ConverterHelper.ConverterSettings = ConverterSettings;
324 ConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> { ConverterHelper.ConvertClassToDictionary(inputClass, null != ConverterSettings.InputClassElementType ? ConverterSettings.InputClassElementType.ToString() : ConverterSettings.InputClassType.ToString(), visited) };
325 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out IGPALGrid<string> outGrid));
326 ConverterSettings.InDataFormat.Add(DataFormat.CLASS);
327 ConverterSettings.InputGrid.Add(outGrid);
328
329 return this;
330 }
331 public IAllowConverterInputAndActions SaveTo<T>(ref T outputClass)
332 {
333 ConverterSettings.OutputFile = null;
334 ConverterSettings.OutputClassType = outputClass.GetType();
335 ConverterSettings.OutputClassElementType = ConverterSettings.OutputClassType.GetElementType();
336
337 if (ConverterSettings.OutputClassElementType == null)
338 {
339 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
340
341 if (genericArgs.Length > 0)
342 {
343 // Check if it's a Dictionary<TKey, TValue>
344 if (ConverterHelper.IsDictionaryType(ConverterSettings.OutputClassType))
345 {
346 ConverterSettings.OutputClassElementType = genericArgs[1]; // TValue
347 }
348 // Check if it's any IGPALGrid<T>
349 else if (ConverterSettings.OutputClassType.IsGenericType &&
350 ConverterSettings.OutputClassType.GetGenericTypeDefinition() == typeof(IGPALGrid<>))
351 {
352 // For IGPALGrid<T>, the "element" type is List<T>
353 var elementT = genericArgs[0]; // T
354 ConverterSettings.OutputClassElementType = typeof(List<>).MakeGenericType(elementT);
355 }
356 // Fallback: assume it's IEnumerable<T> or similar
357 else
358 {
359 ConverterSettings.OutputClassElementType = genericArgs[0];
360 }
361 }
362 }
363
364 ConverterSettings.OutDataFormat = DataFormat.CLASS;
365 Convert(ref outputClass);
366
367 return this;
368 }
374 public IAllowConverterSettingsAndActions WithInputType(DataFormat inDataFormat)
375 {
376 // Every .WithInput leaves one entry per real input, so overriding means replacing the last of them.
377 // Appending made Convert believe there was an extra input, which generated a phantom output file and
378 // then indexed past the inputs that actually exist.
379 // The empty case is index safety only. Reaching it means this was called before any .WithInput, and
380 // .WithInput starts a fresh list, so the value is discarded either way.
381 if (0 == ConverterSettings.InDataFormat.Count)
382 ConverterSettings.InDataFormat.Add(inDataFormat);
383 else
384 ConverterSettings.InDataFormat[ConverterSettings.InDataFormat.Count - 1] = inDataFormat;
385
386 return this;
387 }
388
393 public IAllowConverterSettingsAndActions WithOutputType(DataFormat outDataFormat)
394 {
395 ConverterSettings.OutDataFormat = outDataFormat;
396 return this;
397 }
398
404 {
405 ConverterSettings.FirstLineIsColumnHeaders = firstLineHeaders;
406 ConverterSettings.InputFirstLineIsColumnHeaders = firstLineHeaders;
407 return this;
408 }
409
415 {
416 ConverterSettings.IgnoreFirstLineColumnHeaders = ignoreFirstLine;
417 return this;
418 }
419
424 public IAllowConverterSettingsAndActions WithColumnsEnclosedInQuotes(bool fieldsEnclosedInQuotes = true)
425 {
426 ConverterSettings.FieldsEnclosedInQuotes = fieldsEnclosedInQuotes;
427 return this;
428 }
429
438 public IAllowConverterSettingsAndActions WithColumnNames(List<string> columnNames)
439 {
440 ConverterSettings.ColumnNames = new GPALGrid<string>();
441 if (null != columnNames && 0 < columnNames.Count)
442 ConverterSettings.ColumnNames.AddRow(new List<string>(columnNames));
443 return this;
444 }
445
453 {
454 if (0 == ConverterSettings.ColumnNames.Count())
455 ConverterSettings.ColumnNames.AddRow(new List<string>());
456 ConverterSettings.ColumnNames[0].Add(columnName);
457 return this;
458 }
459
467 private List<string> ResolveEffectiveColumns(List<string> sourceColumns)
468 {
469 List<string> caller = (null != ConverterSettings.ColumnNames && 0 < ConverterSettings.ColumnNames.Count())
470 ? ConverterSettings.ColumnNames[0]
471 : null;
472 if (null == caller || 0 == caller.Count)
473 return (null != sourceColumns) ? new List<string>(sourceColumns) : new List<string>();
474
475 List<string> effective = new List<string>(caller);
476 if (null != sourceColumns)
477 for (int i = effective.Count; i < sourceColumns.Count; i++)
478 effective.Add(sourceColumns[i]);
479 return effective;
480 }
481
490 public IAllowConverterInputAndActions PrettyPrintTo(out string outputData)
491 {
492 ConverterSettings.OutputFile = null;
493 ConverterSettings.OutputClass = null;
494 ConverterSettings.OutputClassType = null;
495 ConverterSettings.OutputClassElementType = null;
496 ConverterSettings.OutputGrid = null;
497
498 // Use the Input format (already known from WithInput) for pretty printing
499 if (ConverterSettings.InDataFormat.FirstOrDefault() != DataFormat.NOTSET)
500 ConverterSettings.OutDataFormat = ConverterSettings.InDataFormat.FirstOrDefault();
501
502 outputData = FormatPretty(ConverterSettings.InputData.FirstOrDefault());
503 ConverterSettings.OutputData = outputData;
504
505 return this;
506 }
507
508 #endregion Fluent Interface
509
528 bool DropAliasesFor(object payload)
529 {
530 int limit = GPAL.GPALSettings.YamlAliasLimit;
531 bool retVal = false;
532
533 if (0 < limit)
534 {
535 int nodes = ConverterHelper.CountGraphNodes(payload, limit, out bool cyclic);
536
537 retVal = false == cyclic && nodes >= limit;
538
539 if (true == cyclic && nodes >= limit)
540 GPAL.PublishSimpleEvent(Enums.GPALEventType.DEBUG, $"The payload holds a cycle, so its aliases are kept whatever its size. Without them YAML cannot write it at all", null, Enums.GPALObjectType.Converter);
541 else if (true == retVal)
542 GPAL.PublishSimpleEvent(Enums.GPALEventType.DEBUG, $"The payload reached the alias limit of [{limit}] nodes and holds no cycle, so a shared reference is written out again rather than as an alias. GPAL.WithYamlAliasLimit moves the limit", null, Enums.GPALObjectType.Converter);
543 }
544
545 return retVal;
546 }
547
558 int InputWithin(int inputIdx, int inputCount, string whatIsMissing)
559 {
560 int within = inputIdx;
561
562 if (inputCount <= inputIdx)
563 {
564 within = inputCount - 1;
565 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Output [{inputIdx + 1}] has no {whatIsMissing} of its own, only [{inputCount}] given. Using the last one.", this, GPALObjectType.Converter);
566 }
567
568 return within;
569 }
570
571 internal IAllowConverterInputAndActions Convert<T>(ref T outputClass, bool appendToFile = false)
572 {
573 CancelIdleTimer();
574 ConverterHelper.ConverterSettings = ConverterSettings;
575
576 #region <Input>
577 IGPALGrid<string> outGrid = GPAL.Grid.ToGPALObject();
578 ConverterSettings.OutputData = null;
579 // NOTE: ColumnNames is NOT cleared here. It is reset per WithInput and may hold caller-supplied
580 // names (WithColumnName/s) that must survive across chained SaveTo calls; the source-derived
581 // fallbacks below only fill it when the caller left it empty.
582
583 if (null != ConverterSettings.InputFile)
584 {
585 if (true == File.Exists(ConverterSettings.InputFile))
586 {
587 if (false == FileHelper.TokenizeFile(ConverterSettings.InputFile, ConverterSettings))
588 {
589 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to tokenize file [{ConverterSettings.InputFile.Filename}]. Continuing.", ConverterSettings, GPALObjectType.Other);
590 return this;
591 }
592 ConverterSettings.InputDictionary = ((IGPALFileInternal)ConverterSettings.InputFile).FileDictionary; // CAVEAT: what about multiple files in the GPALFile?
593 ConverterSettings.InputData = ((IGPALFileInternal)ConverterSettings.InputFile).FileData;
594 ConverterSettings.InputGrid = ((IGPALFileInternal)ConverterSettings.InputFile).FileGrid;
595 // Only fall back to the file's own column list when the caller did not supply columns,
596 // and copy it (never alias) so the file's ColumnList is never mutated by this conversion.
597 if (ConverterHelper.IsDelimitedDataFormat(ConverterHelper.GetDataFormatFromExtension(ConverterSettings.InputFile.Filename))
598 && 0 == ConverterSettings.ColumnNames.Count())
599 {
600 ConverterSettings.ColumnNames = new GPALGrid<string>();
601 foreach (List<string> colRow in ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList)
602 ConverterSettings.ColumnNames.AddRow(new List<string>(colRow));
603 }
604 ConverterSettings.ExcelRowsPerSheet = ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ExcelRowsPerSheet;
605 ConverterSettings.ExcelSheetNames = ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ExcelSheetNames;
606 ConverterSettings.FirstLineIsColumnHeaders = (bool)(0 < ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.FirstLineIsColumnNames.Count() ? ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.FirstLineIsColumnNames?[0] : ConverterSettings.InputFirstLineIsColumnHeaders); // default for non-file output paths; overridden per output file below
607 ConverterSettings.InDelimiter = ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter[0]; // default for non-file output paths; overridden per input file below
608
609 // InDataFormat needs no repopulating here. It is filled by WithInput, one entry per input, and
610 // nothing empties it afterwards: the idle timer's ClearDerived drops the cached payloads, not
611 // the declared formats. So it survives every SaveTo/AppendTo in a chain on its own.
612 }
613 else
614 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"File missing: [{ConverterSettings.InputFile.Filename}]", ConverterSettings, GPALObjectType.Other);
615 }
616 else if (0 < ConverterSettings.InputData.Count) // we were given custom input data or type
617 {
618 for (int idx = 0; idx < ConverterSettings.InputData.Count; idx++)
619 {
620 switch (ConverterSettings.InDataFormat[idx])
621 {
622 case DataFormat.CUSTOM_DELIMITER:
623 case DataFormat.CARET:
624 case DataFormat.CSV:
625 case DataFormat.LOG: // a finished line of text, written and read as it stands
626 case DataFormat.PIPE:
627 case DataFormat.PRN:
628 case DataFormat.TAB:
629 ConverterSettings.InputDictionary.Clear();
630 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertDelimitedToDictionary(ConverterSettings, new StringReader(ConverterSettings.InputData[idx])));
631 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
632 ConverterSettings.InputGrid.Add(outGrid);
633 break;
634 case DataFormat.STRING:
635 ConverterSettings.InputGrid.Add(GPAL.Grid.AddRow(new List<string> { ConverterSettings.InputData[idx] }).ToGPALObject());
636 ConverterSettings.InputDictionary.Clear();
637 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertDelimitedToDictionary(ConverterSettings, new StringReader(ConverterSettings.InputData[idx])));
638 break;
639 case DataFormat.DICTIONARY:
640 break;
641 case DataFormat.JSON:
642 var desObj = JsonConvert.DeserializeObject<dynamic>(ConverterSettings.InputData[idx], new JsonConverter[] { new CustomJsonConverter() });
643 if (null != desObj)
644 {
645 Dictionary<object, dynamic> jsonDict;
646 if (desObj is IDictionary)
647 {
648 jsonDict = (Dictionary<object, dynamic>)desObj;
649 }
650 else if (desObj is IList jsonList)
651 {
652 // Root is a JSON array, not an object - synthesize row keys
653 // (same GPALKEYN convention used for headerless CSV rows and YAML sequence roots).
654 jsonDict = new Dictionary<object, dynamic>();
655 int rootIdx = 0;
656 foreach (var item in jsonList)
657 {
658 jsonDict[$"GPALKEY{rootIdx++:D4}"] = item;
659 }
660 }
661 else
662 {
663 // Root is a bare JSON scalar.
664 jsonDict = new Dictionary<object, dynamic> { ["GPALKEY0000"] = desObj };
665 }
666
667 ConverterSettings.InputDictionary.Add(jsonDict);
668 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
669 ConverterSettings.InputGrid.Add(outGrid);
670 }
671 else
672 return this;
673
674 break;
676 //case DataFormat.PDF:
677 // break;
678 case DataFormat.XLSX:
679 break;
680 case DataFormat.XML:
681 ConverterSettings.InputXmlDocument.PreserveWhitespace = true;
682 ConverterSettings.InputXmlDocument.Load(ConverterSettings.InputData[idx]);
683 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertXMLToDictionary(ConverterSettings.InputXmlDocument));
684 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
685 ConverterSettings.InputGrid.Add(outGrid);
686 break;
687 case DataFormat.YAML:
688 var deserializer = new DeserializerBuilder()
689 .WithNamingConvention(CamelCaseNamingConvention.Instance)
690 .Build();
691
692 dynamic yamlObject = deserializer.Deserialize<object>(ConverterSettings.InputData[idx]);
693 var yamlGridRows = new List<List<string>>();
694 var processedYamlDict = new Dictionary<dynamic, dynamic>();
695
696 if (yamlObject is IDictionary yamlMapping)
697 {
698 foreach (var key in yamlMapping.Keys)
699 {
700 string keyStr = key?.ToString() ?? "null_key";
701 processedYamlDict[keyStr] = FileHelper.ProcessDictionaryValue(yamlMapping[key], yamlGridRows);
702 }
703 }
704 else if (yamlObject is IList yamlSequence)
705 {
706 // Root is a YAML sequence, not a mapping - synthesize row keys
707 // (same GPALKEYN convention used for headerless CSV rows).
708 int rootIdx = 0;
709 foreach (var item in yamlSequence)
710 {
711 processedYamlDict[$"GPALKEY{rootIdx++:D4}"] = FileHelper.ProcessDictionaryValue(item, yamlGridRows);
712 }
713 }
714 else
715 {
716 // Root is a bare YAML scalar.
717 processedYamlDict["GPALKEY0000"] = FileHelper.ProcessDictionaryValue(yamlObject, yamlGridRows);
718 }
719
720 var yamlGridRow = new GPALGrid<string>();
721 foreach (var row in yamlGridRows)
722 {
723 yamlGridRow.AddRow(row);
724 }
725
726 ConverterSettings.InputDictionary.Add(processedYamlDict);
727 ConverterSettings.InputGrid.Add(yamlGridRow);
728 break;
729 case DataFormat.HTML:
730 ConverterSettings.InputDictionary = ConverterHelper.ConvertHtmlToDictionary(ConverterSettings.InputData[idx]);
731 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid));
732 ConverterSettings.InputGrid.Add(outGrid);
733 break;
734 }
735 }
736 }
737 else if (null != ConverterSettings.InputDatabase)
738 {
739 List<string> dbColumns = new List<string>();
740
741 // TODO: convert to other types
742 DatabaseHelper.TokenizeDatabase(null, ConverterSettings.InputDatabase);
743 ConverterSettings.InputGrid.Add(ConverterSettings.InputDatabase.Tokens);
744 if (0 < ((GPALDatabase)ConverterSettings.InputDatabase).DatabaseSettings.ColumnList.Count) // no columns means no results
745 foreach (DatabaseSettings.DatabaseColumn column in ((GPALDatabase)ConverterSettings.InputDatabase).DatabaseSettings.ColumnList)
746 dbColumns.Add(column.ColumnName);
747
748 // Caller-supplied columns override the database's own columns positionally; the database is
749 // just the fallback/backfill, exactly like a file's column list.
750 List<string> columns = ResolveEffectiveColumns(dbColumns);
751 if (0 == ConverterSettings.ColumnNames.Count() && 0 < columns.Count)
752 ConverterSettings.ColumnNames.AddRow(new List<string>(columns));
753
754 ConverterSettings.InputDictionary = ConverterHelper.ConvertGridToDictionary(columns, ConverterSettings.InputDatabase.Tokens);
755 }
756 else if (null != ConverterSettings.InputGrid && 0 < ConverterSettings.InputGrid.Count)
757 {
758 // Caller-supplied columns key the rows. For a grid the "source" names are the browser's
759 // selector names, which the converter cannot see, so the browser tops HeaderList up from
760 // selector names before handing the grid over; any names still missing here are generated.
761 List<string> columns = ResolveEffectiveColumns(null);
762 bool haveColumns = 0 < columns.Count;
763
764 ConverterSettings.InputDictionary.Clear();
765 foreach (IGPALGrid<string> inGrid in ConverterSettings.InputGrid)
766 {
767 // One dictionary per grid (one grid == one input source), rows keyed under GPALKEY - the
768 // same shape delimited-file input produces - keyed by column name when columns are known.
769 Dictionary<object, dynamic> source = new Dictionary<object, dynamic>();
770 int lineNumber = 0;
771 foreach (List<string> gridRow in inGrid)
772 {
773 if (haveColumns)
774 {
775 Dictionary<string, string> rowDict = new Dictionary<string, string>();
776 for (int i = 0; i < gridRow.Count; i++)
777 rowDict[i < columns.Count ? columns[i] : $"Column{i + 1}"] = gridRow[i];
778 source[$"GPALKEY{lineNumber:D4}"] = rowDict;
779 }
780 else
781 source[$"GPALKEY{lineNumber:D4}"] = new List<string>(gridRow);
782 lineNumber++;
783 }
784 ConverterSettings.InputDictionary.Add(source);
785 }
786 }
787 else if (null != ConverterSettings.InputClass && 0 < ConverterSettings.InDataFormat.Count)
788 {
789 // Re-derive from persisted InputClass for chained SaveTo calls (same role as InputFile re-tokenization above)
790 HashSet<object> visited = new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
791 IGPALGrid<string> repopGrid;
792 ConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> {
793 ConverterHelper.ConvertClassToDictionary(
794 ConverterSettings.InputClass,
795 null != ConverterSettings.InputClassElementType
796 ? ConverterSettings.InputClassElementType.ToString()
797 : ConverterSettings.InputClassType.ToString(),
798 visited)
799 };
800 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out repopGrid));
801 ConverterSettings.InDataFormat.Add(DataFormat.CLASS);
802 ConverterSettings.InputGrid.Add(repopGrid);
803 }
804 #endregion <Input>
805 #region <Output>
806 StringBuilder stringBuilder = new StringBuilder();
807 if (null != ConverterSettings.OutputFile)
808 {
809 // Pair each output file with an input "dataset" (one per input file, or one for class/grid/database/etc input):
810 // output[i] is converted from input[min(i, inputFileCount-1)] - the last input services any extra outputs.
811 // If there are more inputs than outputs, grow the output filename list using the last user-supplied
812 // output filename as a naming pattern (name_2.ext, name_3.ext, ...).
813 int inputFileCount = Math.Max(1, ConverterSettings.InDataFormat.Count);
814 GPALFileSettings outputFileSettings = ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings;
815
816 // worked out fresh for this conversion and never written back. Filenames holds what the workflow
817 // supplied and nothing else, so the pattern is always a real filename rather than one of these,
818 // and a GPALFile reused across conversions does not grow a longer list every time
819 List<string> outputPaths = new List<string>(outputFileSettings.Filenames);
820
821 if (0 < outputPaths.Count && outputPaths.Count < inputFileCount)
822 {
823 string pattern = outputPaths[outputPaths.Count - 1];
824 string patternDirectory = Path.GetDirectoryName(pattern);
825 string patternName = Path.GetFileNameWithoutExtension(pattern);
826 string patternExtension = Path.GetExtension(pattern);
827
828 for (int newFileIdx = outputPaths.Count; newFileIdx < inputFileCount; newFileIdx++)
829 {
830 string generatedFilename = Path.Combine(patternDirectory, $"{patternName}_{newFileIdx + 1}{patternExtension}");
831 outputPaths.Add(generatedFilename);
832 ConverterSettings.OutputFile.ReturnFilenames.Add(generatedFilename);
833 }
834 }
835
836 int fileIdx = 0;
837 DataFormat savedOutDataFormat = ConverterSettings.OutDataFormat;
838 foreach (string filename in outputPaths)
839 {
840 int inputIdx = Math.Min(fileIdx, inputFileCount - 1);
841
842 // each input file may have its own delimiter (e.g. mixing .csv and .tab inputs) - use the one paired with this output
843 if (null != ConverterSettings.InputFile && inputIdx < ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter.Count)
844 ConverterSettings.InDelimiter = ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter[inputIdx];
845
846 // if this is set going into this, we are being asked to override figuring it out for each file and will use this one set by the programmer
847 if (DataFormat.NOTSET == savedOutDataFormat)
848 ConverterSettings.OutDataFormat = ConverterHelper.GetDataFormatFromExtension(filename);
849
850 /* - not sure, assume the output delim is the same as input? seems outdated logic
851 if (0 < ConverterSettings.OutputFile.FileSettings.Delimiter.Count() && true == ConverterSettings.OutputFile.FileSettings.Delimiter[fileIdx].HasValue)
852 ConverterSettings.OutDelimiter = ConverterSettings.OutputFile.FileSettings.Delimiter[fileIdx].Value;
853 else
854 */
855 if (fileIdx < ((IGPALFileInternal)ConverterSettings.OutputFile).FirstLineIsColumnNames.Count)
856 // An explicit output-file setting (true OR false) always wins - this is how the user
857 // turns the header row off despite having named columns.
858 ConverterSettings.FirstLineIsColumnHeaders = ((IGPALFileInternal)ConverterSettings.OutputFile).FirstLineIsColumnNames[fileIdx];
859 else
860 // No explicit setting: emit a header when we have column names to write (named via
861 // WithHeader / WithColumnName(s), or resolved from the source), or when the caller
862 // declared headers via WithFirstLineHasColumnNames. Naming columns implies wanting them.
863 ConverterSettings.FirstLineIsColumnHeaders =
864 (null != ConverterSettings.ColumnNames && 0 < ConverterSettings.ColumnNames.Count())
865 || ConverterSettings.InputFirstLineIsColumnHeaders;
866
867 // Headers are never repeated when appending to an existing file.
868 if (true == appendToFile)
869 ConverterSettings.FirstLineIsColumnHeaders = false;
870
871 if (fileIdx < ((IGPALFileInternal)ConverterSettings.OutputFile).FieldsEnclosedInQuotes.Count)
872 ConverterSettings.FieldsEnclosedInQuotes = ((IGPALFileInternal)ConverterSettings.OutputFile).FieldsEnclosedInQuotes[fileIdx];
873
874 if (null == ConverterSettings.OutDelimiter)
875 ConverterSettings.OutDelimiter = ConverterHelper.GetDelimiterFromFormat(ConverterSettings.OutDataFormat);
876
877 if (null != ConverterSettings.ColumnNames && 0 == ConverterSettings.ColumnNames.Count())
878 {
879 List<string> columns = new List<string>();
880
881 if ((ConverterHelper.IsDelimitedDataFormat(ConverterSettings.OutDataFormat) || DataFormat.XLSX == ConverterSettings.OutDataFormat) && null != ConverterSettings.OutputFile && null != ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings.ColumnList && fileIdx < ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings.ColumnList.Count())
882 foreach (string columnName in ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings.ColumnList[fileIdx])
883 columns.Add(columnName);
884 else if (inputIdx < ConverterSettings.InDataFormat.Count && (ConverterHelper.IsDelimitedDataFormat(ConverterSettings.InDataFormat[inputIdx]) || DataFormat.XLSX == ConverterSettings.InDataFormat[inputIdx]) && null != ConverterSettings.InputFile && null != ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList && inputIdx < ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList.Count())
885 foreach (string columnName in ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList[inputIdx])
886 columns.Add(columnName);
887
888 if (0 < columns.Count)
889 ConverterSettings.ColumnNames.AddRow(columns);
890 }
891
892 switch (ConverterSettings.OutDataFormat)
893 {
894 case DataFormat.CUSTOM_DELIMITER:
895 case DataFormat.CARET:
896 case DataFormat.CSV:
897 case DataFormat.LOG: // a finished line of text, written and read as it stands
898 case DataFormat.PIPE:
899 case DataFormat.PRN:
900 case DataFormat.TAB:
901 case DataFormat.NOTSET:
902 if (0 < ConverterSettings.InDataFormat.Count)
903 {
904 switch (ConverterSettings.InDataFormat[inputIdx])
905 {
906 case DataFormat.CARET:
907 case DataFormat.COLON:
908 case DataFormat.CSV:
909 case DataFormat.CUSTOM_DELIMITER:
910 case DataFormat.DOT:
911 //case DataFormat.HTML:
912 case DataFormat.HYPHEN:
913 case DataFormat.LOG: // a finished line of text, written and read as it stands
914 case DataFormat.PIPE:
915 case DataFormat.SEMICOLON:
916 case DataFormat.SPACE:
917 case DataFormat.TAB:
918 case DataFormat.PRN:
919 if (inputIdx < ConverterSettings.InputData.Count)
920 ConverterSettings.OutputData = ConverterSettings.InputData[inputIdx];
921 else
922 ConverterSettings.OutputData = File.ReadAllText(ConverterSettings.InputFile.Filenames[inputIdx]);
923 if (null != ConverterSettings.OutputData && null != ConverterSettings.InDelimiter && true == ConverterSettings.InDelimiter.HasValue)
924 {
925 using (StringReader reader = new StringReader(ConverterSettings.OutputData))
926 {
927 string line;
928 StringBuilder lineBuilder = new StringBuilder();
929 while ((line = reader.ReadLine()) != null)
930 {
931 string[] tokens = line.Split(ConverterSettings.InDelimiter.Value);
932 foreach (string str in tokens)
933 stringBuilder.Append(ConverterHelper.CreateCSVToken(str, ConverterSettings)); // if these should be wrapped in "", createcsvtoken will do that
934 if (0 < stringBuilder.Length)
935 stringBuilder.Remove(stringBuilder.Length - 1, 1); // remove trailing inDelimiter
936 lineBuilder.AppendLine(stringBuilder.ToString());
937 stringBuilder.Clear();
938 }
939 ConverterSettings.OutputData = lineBuilder.ToString();
940 }
941 }
942 stringBuilder.Append(ConverterSettings.OutputData);
943 break;
944 case DataFormat.HTML:
945 //stringBuilder.Append(ConverterSettings.InputData[fileIdx]); // NOTE: this feels like the wrong variable name
946 //break;
947 case DataFormat.CLASS:
948 default: // nested structure? use dictionary - maybe we can always use the dictionary?
949 stringBuilder.Append(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid));
950 break;
951 }
952 if (true == appendToFile)
953 File.AppendAllText(filename, stringBuilder.ToString());
954 else
955 File.WriteAllText(filename, stringBuilder.ToString());
956 }
957 else // not sure this will ever hit...
958 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"File [{ConverterSettings.InputFile}] does not exist(?) as no DATA FORMAT has been set.", this, GPALObjectType.Converter);
959 break;
960 case DataFormat.JSON:
961 if (ConverterSettings.InputDictionary.Count > 0)
962 {
963 int dictIdx = InputWithin(inputIdx, ConverterSettings.InputDictionary.Count, "input");
964 var rootDict = ConverterSettings.InputDictionary[dictIdx] as IDictionary;
965
966 object rawObject;
967
968 if (rootDict != null && ConverterHelper.IsArrayLike(rootDict))
969 {
970 var list = new List<object>();
971 foreach (DictionaryEntry e in rootDict)
972 {
973 string k = e.Key.ToString();
974 if (k.StartsWith("GPALKEY"))
975 {
976 list.Add(e.Value);
977 }
978 }
979 rawObject = list;
980 }
981 else
982 {
983 rawObject = rootDict ?? ConverterSettings.InputDictionary[dictIdx];
984 }
985
986 dynamic cleanObject = ConverterHelper.NormalizeForCleanJson(rawObject);
987 cleanObject = ConverterHelper.StripGpalKeys(cleanObject);
988
989 var settings = new JsonSerializerSettings
990 {
991 // Your existing settings, e.g. NamingStrategy if needed
992 Converters =
993 {
994 new IPAddressJsonConverter(),
995 new ComplexJsonConverter(),
996 new UriJsonConverter(),
997 new VersionJsonConverter(),
998 // new BigIntegerJsonConverter(), // only if needed
999 // Add any others you already have (e.g. for quoted strings)
1000 },
1001 ReferenceLoopHandling = ReferenceLoopHandling.Ignore, // ignore self referencing classes/structs as an error
1002 Formatting = Newtonsoft.Json.Formatting.Indented // optional, for readable output
1003 };
1004
1005 ConverterSettings.OutputData = JsonConvert.SerializeObject(cleanObject, settings);
1006
1007 if (appendToFile)
1008 File.AppendAllText(filename, ConverterSettings.OutputData);
1009 else
1010 File.WriteAllText(filename, ConverterSettings.OutputData);
1011 }
1012 else
1013 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1014 break;
1016 //case DataFormat.PDF:
1017 // break;
1018 case DataFormat.XLSX:
1019 if (inputIdx < ConverterSettings.InputGrid.Count)
1020 ExcelHelper.WriteXlsxFromTokenizedData(
1021 outputPath: filename,
1022 grid: ConverterSettings.InputGrid[inputIdx], // clone if you don't want the helper to have a reference to your live grid
1023 rowsPerSheet: ConverterSettings.ExcelRowsPerSheet[inputIdx],
1024 columnNamesPerSheet: ConverterSettings.ColumnNames,
1025 sheetNames: ConverterSettings.ExcelSheetNames[inputIdx],
1026 writeHeaders: ConverterSettings.FirstLineIsColumnHeaders // or true if you always want headers
1027 );
1028 else
1029 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1030 break;
1031 case DataFormat.XML:
1032 if (0 < ConverterSettings.InputDictionary.Count)
1033 {
1034 ConverterSettings.OutputXmlDocument = new XmlDocument();
1035
1036 // Convert the dictionary to XML using recursion
1037 var rootElement = ConverterSettings.OutputXmlDocument.CreateElement("root");
1038 ConverterSettings.OutputXmlDocument.AppendChild(rootElement);
1039
1040 // get existing file, find root, then start adding to it
1041 if (true == appendToFile)
1042 {
1043 XmlDocument xmlDocument = new XmlDocument();
1044 try
1045 {
1046 xmlDocument.Load(filename);
1047 ConverterSettings.OutputXmlDocument = xmlDocument;
1048 rootElement = (XmlElement)xmlDocument.GetElementsByTagName("root")[0];
1049 }
1050 catch (Exception ex)
1051 {
1052 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"File [{filename}] does not exist to append XML to, writing new.", this, GPALObjectType.Converter, ex);
1053 }
1054 }
1055
1056 int dictCount = 0;
1057 foreach (Dictionary<object, dynamic> dict in ConverterSettings.InputDictionary)
1058 {
1059 if (DataFormat.HTML == ConverterSettings.InDataFormat[dictCount])
1060 ConverterHelper.ConvertHtmlDictionaryToXml(dict, rootElement); // append to rootElement in method
1061 else if (DataFormat.XML == ConverterSettings.InDataFormat[dictCount])
1062 ConverterHelper.ConvertDictionaryToXmlElement(dict, rootElement, false); // append to rootElement in method
1063 else
1064 ConverterHelper.ConvertDictionaryToXmlElement(dict, rootElement); // append to rootElement in method
1065 dictCount++;
1066 }
1067
1068 // NOTE: Preserve original XML header(s)
1069 string inputFilename = ConverterSettings.InputFile?.Filename; // the original source file
1070
1071 if (true == ConverterSettings.InputFile?.Filename.ToLower().EndsWith("xml"))
1072 {
1073 string header = ConverterHelper.GetXmlHeaderFromFile(inputFilename);
1074
1075 // Write header from original document
1076 if (0 < header.Length)
1077 using (var writer = new StreamWriter(filename, append: false))
1078 {
1079 writer.Write(header);
1080
1081 using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings
1082 {
1083 OmitXmlDeclaration = true,
1084 Indent = true,
1085 Encoding = writer.Encoding
1086 }))
1087 {
1088 ConverterSettings.OutputXmlDocument.Save(xmlWriter);
1089 }
1090 }
1091 else
1092 ConverterSettings.OutputXmlDocument.Save(filename);
1093 }
1094 else
1095 ConverterSettings.OutputXmlDocument.Save(filename);
1096 }
1097 break;
1098 case DataFormat.YAML:
1099 if (ConverterSettings.InputClass != null)
1100 {
1101 var serializerBuilder = new SerializerBuilder()
1102 .WithObjectGraphTraversalStrategyFactory((ti, tr, tc, depth)
1103 => new FullObjectGraphTraversalStrategy(ti, tr, depth, CamelCaseNamingConvention.Instance, new DefaultObjectFactory()))
1104 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1105 .WithEventEmitter(next => new ExplicitNullYamlEventEmitter(next))
1106 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
1107 .WithTypeConverter(new IPAddressYamlConverter())
1108 .WithTypeConverter(new ComplexYamlConverter())
1109 .WithTypeConverter(new UriYamlConverter())
1110 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1111 .WithTypeConverter(new NullableGuidYamlConverter())
1112 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1113 .WithTypeConverter(new VersionYamlConverter())
1114 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1115 .WithTypeConverter(new QuotedStringYamlConverter())
1116 .WithTypeConverter(new WaitTimeYamlConverter());
1117
1118 if (true == DropAliasesFor(ConverterSettings.InputClass))
1119 serializerBuilder = serializerBuilder.DisableAliases();
1120
1121 var serializer = serializerBuilder.Build();
1122
1123 ConverterSettings.OutputData = serializer.Serialize(ConverterSettings.InputClass);
1124
1125 if (true == appendToFile)
1126 File.AppendAllText(filename, ConverterSettings.OutputData);
1127 else
1128 File.WriteAllText(filename, ConverterSettings.OutputData);
1129 }
1130 else if (0 < ConverterSettings.InputDictionary.Count)
1131 {
1132 int yamlIdx = InputWithin(inputIdx, ConverterSettings.InputDictionary.Count, "input");
1133 bool largeGraph = DropAliasesFor(ConverterSettings.InputDictionary[yamlIdx]);
1134
1135 var serializerBuilder = new SerializerBuilder()
1136 .WithEventEmitter(next => new ExplicitNullYamlEventEmitter(next))
1137 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
1138 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1139 .WithTypeConverter(new IPAddressYamlConverter())
1140 .WithTypeConverter(new ComplexYamlConverter())
1141 .WithTypeConverter(new UriYamlConverter())
1142 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1143 .WithTypeConverter(new NullableGuidYamlConverter())
1144 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1145 .WithTypeConverter(new VersionYamlConverter())
1146 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1147 .WithTypeConverter(new QuotedStringYamlConverter())
1148 .WithTypeConverter(new WaitTimeYamlConverter());
1149
1150 if (true == largeGraph)
1151 serializerBuilder = serializerBuilder.DisableAliases();
1152
1153 var serializer = serializerBuilder.Build();
1154
1155 object CleanForYaml(object input)
1156 {
1157 // a row built from column names is a Dictionary<string, string>, which is not a
1158 // Dictionary<object, dynamic>, so it missed the mapping branch below and fell into
1159 // the enumerable one, where a dictionary enumerates as the key/value pairs it holds
1160 // and serialized as those pairs instead of as a mapping
1161 if (input is IDictionary otherShapedDict && false == (input is Dictionary<object, dynamic>))
1162 {
1163 Dictionary<object, dynamic> asGpalDict = new Dictionary<object, dynamic>();
1164
1165 foreach (DictionaryEntry entry in otherShapedDict)
1166 asGpalDict[entry.Key] = entry.Value;
1167
1168 input = asGpalDict;
1169 }
1170
1171 if (input is Dictionary<object, dynamic> d)
1172 {
1173 // First: detect if this dict came from XML (has @keys or #text)
1174 bool isXmlDerived = d.Keys.Cast<object>().Any(k =>
1175 k.ToString() == "#text" || k.ToString().StartsWith("@"));
1176
1177 if (ConverterHelper.IsArrayLike(d))
1178 {
1179 // Surrogate array - collect in order
1180 var list = new List<object>();
1181 foreach (var p in d)
1182 {
1183 string k = p.Key.ToString();
1184
1185 if (k.StartsWith("GPALKEY"))
1186 {
1187 list.Add(CleanForYaml(p.Value));
1188 }
1189 }
1190 return list;
1191 }
1192 else
1193 {
1194 // Normal object
1195 var cleanDict = new Dictionary<object, dynamic>();
1196
1197 string textValue = null;
1198 if (d.TryGetValue("#text", out var txt) && isXmlDerived)
1199 {
1200 textValue = txt?.ToString();
1201 }
1202
1203 // Collect attributes (remove @ prefix)
1204 foreach (var p in d)
1205 {
1206 string k = p.Key.ToString();
1207
1208 k = k.Replace(@"GPAL_", "");
1209
1210 if (k.StartsWith("@") && isXmlDerived)
1211 {
1212 cleanDict[k.Substring(1)] = p.Value;
1213 }
1214 }
1215
1216 // Collect child elements
1217 foreach (var p in d)
1218 {
1219 string k = p.Key.ToString();
1220
1221 k = k.Replace(@"GPAL_", "");
1222
1223 if (k == "#text" || k.StartsWith("@") || k.StartsWith("GPALKEY"))
1224 continue;
1225
1226 cleanDict[k] = CleanForYaml(p.Value);
1227 }
1228
1229 // Flatten leaf nodes: if only text + attributes -> add "value"
1230 if (isXmlDerived && textValue != null)
1231 {
1232 if (cleanDict.Count == 0)
1233 {
1234 // Pure text node -> flatten to scalar
1235 if (double.TryParse(textValue, out double num))
1236 return num;
1237 return textValue;
1238 }
1239 else
1240 {
1241 // Has attributes -> add value field
1242 cleanDict["value"] = textValue;
1243 }
1244 }
1245
1246 return cleanDict;
1247 }
1248 }
1249 else if (input is IEnumerable enumerable && !(input is string))
1250 {
1251 var list = new List<object>();
1252 foreach (var item in enumerable)
1253 {
1254 list.Add(CleanForYaml(item));
1255 }
1256 return list;
1257 }
1258
1259 return input;
1260 }
1261
1262 Dictionary<object, dynamic> rootValue = ConverterSettings.InputDictionary[yamlIdx];
1263 dynamic cleanRoot = CleanForYaml(rootValue);
1264 cleanRoot = ConverterHelper.StripGpalKeys(cleanRoot);
1265
1266 if (true == ConverterHelper.IsEnumerableObject(cleanRoot) && !(cleanRoot is IDictionary) && !(cleanRoot is string))
1267 {
1268 cleanRoot = new Dictionary<object, object>
1269 {
1270 ["values"] = cleanRoot
1271 };
1272 }
1273
1274 ConverterSettings.OutputData = serializer.Serialize(cleanRoot);
1275
1276 if (true == appendToFile)
1277 File.AppendAllText(filename, ConverterSettings.OutputData);
1278 else
1279 File.WriteAllText(filename, ConverterSettings.OutputData);
1280 }
1281 else
1282 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1283 break;
1284 case DataFormat.HTML:
1285 if (0 < ConverterSettings.InputDictionary.Count)
1286 {
1287 switch (ConverterSettings.InDataFormat[inputIdx])
1288 {
1289 case DataFormat.CARET:
1290 case DataFormat.COLON:
1291 case DataFormat.CSV:
1292 case DataFormat.CUSTOM_DELIMITER:
1293 case DataFormat.DOT:
1294 case DataFormat.HYPHEN:
1295 case DataFormat.LOG: // a finished line of text, written and read as it stands
1296 case DataFormat.PIPE:
1297 case DataFormat.SEMICOLON:
1298 case DataFormat.SPACE:
1299 case DataFormat.TAB:
1300 case DataFormat.PRN:
1301 {
1302 var sb = new StringBuilder();
1303
1304 var grid = ConverterSettings.InputGrid; // List<List<string>>
1305 if (grid == null || grid.Count == 0)
1306 {
1307 ConverterSettings.OutputData = "<p>No data</p>";
1308 break;
1309 }
1310
1311 int gridIdx = InputWithin(inputIdx, grid.Count, "input grid");
1312
1313 sb.AppendLine("<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; font-family: Arial, sans-serif; width: 100%;\">");
1314
1315 // Determine headers
1316 List<string> headers = null;
1317 int dataStartRow = 0;
1318
1319 if (ConverterSettings.ColumnNames?.Count() > fileIdx && ConverterSettings.ColumnNames[fileIdx].Count > 0)
1320 {
1321 headers = ConverterSettings.ColumnNames[fileIdx];
1322 dataStartRow = 0;
1323 }
1324 else if (ConverterSettings.FirstLineIsColumnHeaders && grid.Count > 0)
1325 {
1326 headers = grid[gridIdx][0];
1327 dataStartRow = 1;
1328 }
1329
1330 // Emit <thead> if we have headers
1331 if (headers != null)
1332 {
1333 sb.AppendLine(" <thead>");
1334 sb.AppendLine(" <tr>");
1335 foreach (var h in headers)
1336 {
1337 string safeHeader = System.Net.WebUtility.HtmlEncode(h ?? "");
1338 sb.AppendLine($" <th>{safeHeader}</th>");
1339 }
1340 sb.AppendLine(" </tr>");
1341 sb.AppendLine(" </thead>");
1342 }
1343
1344 // Emit <tbody>
1345 sb.AppendLine(" <tbody>");
1346
1347 for (int rowIdx = dataStartRow; rowIdx < grid[gridIdx].Count(); rowIdx++)
1348 {
1349 var row = grid[gridIdx][rowIdx];
1350 sb.AppendLine(" <tr>");
1351
1352 // Pad or truncate row to match column count if headers exist
1353 int colCount = headers?.Count ?? row.Count();
1354 for (int colIdx = 0; colIdx < colCount; colIdx++)
1355 {
1356 string cell = (colIdx < row.Count()) ? (row[colIdx].ToString() ?? "") : "";
1357 cell = cell.Trim(ConverterSettings.InDelimiter.Value);
1358 string safeCell = System.Net.WebUtility.HtmlEncode(cell);
1359 sb.AppendLine($" <td>{safeCell}</td>");
1360 }
1361
1362 sb.AppendLine(" </tr>");
1363 }
1364
1365 sb.AppendLine(" </tbody>");
1366 sb.AppendLine("</table>");
1367
1368 ConverterSettings.OutputData = sb.ToString();
1369 }
1370 break;
1371 default:
1372 foreach (Dictionary<object, dynamic> dict in ConverterSettings.InputDictionary)
1373 {
1374 // TODO: CAVEAT: not sure what is going on, we seem to be duplicating the page in our dictionary, so skip if we already wrote out </body>
1375 // Fix loading the dictioanry with multiple copies
1376 if (true == ConverterSettings.OutputData?.Contains("</body>"))
1377 break;
1378
1379 try
1380 {
1381 if (dict["tag"].ToString() == "#text") // htmlnodes contain text as child nodes, but also a duplicate #text tag following every tag
1382 continue;
1383 }
1384 catch
1385 {
1386 // it's ok, move on...
1387 }
1388 stringBuilder.AppendLine(ConverterHelper.ConvertDictionaryToHtml(dict));
1389
1390 if (0 < stringBuilder.Length)
1391 {
1392 ConverterSettings.OutputData += stringBuilder.ToString();
1393 stringBuilder.Clear();
1394 }
1395 }
1396 break;
1397 }
1398
1399 if (true == appendToFile)
1400 File.AppendAllText(filename, ConverterSettings.OutputData);
1401 else
1402 File.WriteAllText(filename, ConverterSettings.OutputData);
1403 }
1404 else
1405 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1406 break;
1407 }
1408 fileIdx++;
1409 }
1410 }
1411 else if (null != ConverterSettings.OutputDatabase)
1412 {
1413 // a grid is rows of parameters and the write runs once per row, so a row holding two values is
1414 // two parameters. an input with no grid behind it, a plain string, has no rows at all and used to
1415 // index an empty list here, so it becomes one row holding that value: the single column case
1416 IGPALGrid<string> parameterGrid;
1417
1418 if (0 < ConverterSettings.InputGrid.Count && null != ConverterSettings.InputGrid[0])
1419 parameterGrid = ConverterSettings.InputGrid[0];
1420 else
1421 {
1422 parameterGrid = GPAL.Grid.ToGPALObject();
1423
1424 foreach (string inputValue in ConverterSettings.InputData)
1425 parameterGrid.AddRow(new List<string> { inputValue });
1426 }
1427
1428 ConverterSettings.OutputDatabase.Create.ClearParameters.WithParameterGrid(parameterGrid).Execute(out int rowCOunt);
1429 }
1430 else if (DataFormat.GRID == ConverterSettings.OutDataFormat && null != ConverterSettings.OutputGrid
1431 && (null != ConverterSettings.InputFile || true == ConverterSettings.InDataFormat.Contains(DataFormat.GRID)))
1432 {
1433 // rows in, rows out. the object output below reaches a caller's grid through a copy gated on its
1434 // own converted result having items, and for a grid that result is empty, so a file read into a
1435 // grid answered with nothing at all. these are the rows as they were read, and they have to be
1436 // taken here rather than after Convert returns because the settings are cleared on the way out
1437 foreach (IGPALGrid<string> inGrid in ConverterSettings.InputGrid)
1438 foreach (List<string> gridRow in inGrid)
1439 ConverterSettings.OutputGrid.AddRow(new List<string>(gridRow));
1440 }
1441 else if (null != ConverterSettings.OutputClassType)
1442 {
1443 bool yamlDirectDone = false;
1444 if (ConverterSettings.InDataFormat.Count > 0 && ConverterSettings.InDataFormat[0] == DataFormat.YAML
1445 && ConverterSettings.InputData.Count > 0)
1446 {
1447 // Struct types can't be pre-allocated during deserialization, so a YAML alias that
1448 // points back through a struct creates infinite recursion — StackOverflowException
1449 // which can't be caught. Pre-scan for aliases; if any exist fall through to COFD.
1450 bool hasAliases = false;
1451 var preParser = new Parser(new System.IO.StringReader(ConverterSettings.InputData[0]));
1452 while (preParser.MoveNext())
1453 {
1454 if (preParser.Current is AnchorAlias)
1455 {
1456 hasAliases = true;
1457 break;
1458 }
1459 }
1460
1461 if (!hasAliases)
1462 {
1463 try
1464 {
1465 var deserializer = new DeserializerBuilder()
1466 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1467 .WithTypeConverter(new IPAddressYamlConverter())
1468 .WithTypeConverter(new ComplexYamlConverter())
1469 .WithTypeConverter(new UriYamlConverter())
1470 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1471 .WithTypeConverter(new NullableGuidYamlConverter())
1472 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1473 .WithTypeConverter(new VersionYamlConverter())
1474 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1475 .WithTypeConverter(new QuotedStringYamlConverter())
1476 .WithTypeConverter(new WaitTimeYamlConverter())
1477 .IgnoreUnmatchedProperties()
1478 .Build();
1479
1480 ConverterSettings.OutputClass = deserializer.Deserialize(ConverterSettings.InputData[0], ConverterSettings.OutputClassType);
1481 yamlDirectDone = true;
1482 }
1483 catch { }
1484 }
1485 }
1486
1487 if (!yamlDirectDone)
1488 {
1489 // XML input arrives with GPAL numeric key prefixes and all values wrapped in
1490 // List<Dict> + #text nodes. Strip/flatten the whole structure once before
1491 // any POCO reconstruction so CreateObjectFromDictionary sees clean keys and scalars.
1492 object classInput = ConverterSettings.InputDictionary;
1493 if (ConverterSettings.InDataFormat.Count > 0 && ConverterSettings.InDataFormat[0] == DataFormat.XML)
1494 classInput = ConverterHelper.StripGpalKeys(ConverterSettings.InputDictionary);
1495
1496 if (true == ConverterSettings.OutputClassType.IsGenericType && true == ConverterHelper.IsDictionaryType(ConverterSettings.OutputClassType) && 0 < ConverterSettings.InputDictionary.Count)
1497 {
1498 ConverterSettings.OutputClass = ConverterHelper.ConvertToClass(classInput, ConverterSettings.OutputClassType, classInput, ConverterSettings.ColumnNames?[0]); // NOTE: CAVEAT: hardcoded value
1499 }
1500 else if (true == ConverterSettings.OutputClassType.IsGenericType && null != ConverterSettings.OutputClassType.GetInterface("IEnumerable"))
1501 {
1502 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1503 if (ConverterSettings.OutputClassElementType == typeof(string))
1504 {
1505 var resultList = new List<string>();
1506 foreach (var dict in ConverterSettings.InputDictionary)
1507 {
1508 resultList.AddRange(BuildHierarchicalStrings(dict));
1509 }
1510 ConverterSettings.OutputClass = resultList;
1511 }
1512 else if (true == ConverterSettings.OutputClassElementType.IsArray)
1513 {
1514 try
1515 {
1516 // Ensure OutputClassElementType is valid
1517 Type elementType = ConverterSettings.OutputClassElementType;
1518 if (elementType == null && ConverterSettings.OutputClassType.IsGenericType)
1519 {
1520 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
1521 elementType = genericArgs.Length > 1 ? genericArgs[1] : genericArgs.FirstOrDefault() ?? typeof(object);
1522 }
1523 if (elementType == null)
1524 {
1525 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"OutputClassElementType is null for [{ConverterSettings.OutputClassType}]", null, GPALObjectType.Other);
1526 ConverterSettings.OutputClass = default(T);
1527 return this;
1528 }
1529
1530 var mi = typeof(ConverterHelper).GetMethod("CreateIEnumerableFromClass",
1531 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1532 null,
1533 new[] { typeof(ConverterSettings) },
1534 null);
1535
1536 var genericMethod = mi.MakeGenericMethod(new[] { elementType });
1537 ConverterSettings.OutputClass = (T)genericMethod.Invoke(null, new object[] { ConverterSettings });
1538 }
1539 catch (Exception ex)
1540 {
1541 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to create IEnumerable for [{ConverterSettings.OutputClassType}]: [{ex.Message}]", null, GPALObjectType.Other, ex);
1542 ConverterSettings.OutputClass = default(T);
1543 return this;
1544 }
1545 }
1546 else
1547 {
1548 try
1549 {
1550 // Ensure OutputClassElementType is valid
1551 Type elementType = ConverterSettings.OutputClassElementType;
1552 if (elementType == null && ConverterSettings.OutputClassType.IsGenericType)
1553 {
1554 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
1555 elementType = genericArgs.Length > 1 ? genericArgs[1] : genericArgs.FirstOrDefault() ?? typeof(object);
1556 }
1557 if (elementType == null)
1558 {
1559 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"OutputClassElementType is null for [{ConverterSettings.OutputClassType}]", null, GPALObjectType.Other);
1560 ConverterSettings.OutputClass = default(T);
1561 return this;
1562 }
1563
1564 // Get the MethodInfo for ConvertToClass
1565 var mi = typeof(ConverterHelper).GetMethod(
1566 "ConvertToClass",
1567 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1568 null,
1569 new[] { typeof(object), typeof(Type), typeof(object), typeof(List<string>) }, // Parameters dynamic dictList, Type targetType, dynamic parentDictionary, List<string> columnNames
1570 null);
1571
1572 // Invoke the method
1573 ConverterSettings.OutputClass = mi.Invoke(null, new object[] { ConverterSettings.InputDictionary, ConverterSettings.OutputClassType, ConverterSettings.InputDictionary, ConverterSettings.ColumnNames?[0] }); // NOTE: CAVEAT hardcoded value
1574 }
1575 catch (Exception ex)
1576 {
1577 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to create IEnumerable for [{ConverterSettings.OutputClassType}]: [{ex.Message}]", null, GPALObjectType.Other, ex);
1578 ConverterSettings.OutputClass = default(T);
1579 return this;
1580 }
1581 }
1582 MethodInfo addMethod = ConverterSettings.OutputClassType.GetMethod("Add",
1583 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1584 MethodInfo addRowMethod = ConverterSettings.OutputClassType.GetMethod("AddRow",
1585 BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(List<>).MakeGenericType(ConverterSettings.OutputClassElementType) }, null);
1586 MethodInfo enqueueMethod = ConverterSettings.OutputClassType.GetMethod("Enqueue",
1587 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1588 MethodInfo pushMethod = ConverterSettings.OutputClassType.GetMethod("Push",
1589 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1590
1591 // It's an IEnumerable<OutputClass>
1592 int count = 0;
1593 try
1594 {
1595 count = ConverterSettings.OutputClass.Count;
1596 }
1597 catch
1598 {
1599 foreach (dynamic item in ConverterSettings.OutputClass)
1600 count++;
1601 }
1602
1603 try
1604 {
1605 if (0 < count)
1606 {
1607 if (null != addRowMethod) // GPAGrid
1608 {
1609 if (null == ConverterSettings.InputData && null != ConverterSettings.InputFile)
1610 {
1611 string directory = Path.GetDirectoryName(ConverterSettings.InputFile.Filenames[0]);
1612 string filenamepart = Path.GetFileName(ConverterSettings.InputFile.Filenames[0]);
1613
1614 string filename2 = Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart).FirstOrDefault();
1615 if (false == string.IsNullOrEmpty(filename2))
1616 {
1617 try
1618 {
1619 ConverterSettings.InputData.Add(File.ReadAllText(filename2));
1620 }
1621 catch (Exception ex)
1622 {
1623 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to read entire file [{filename2}] into memory. Output may be empty depending on output type.", this, GPALObjectType.Converter, ex);
1624 }
1625 }
1626 }
1627
1628 // COPY ConverterSettings.OutputClass to the ref outputClass.
1629 // once, not once per input: the inputs were read and merged at the top of
1630 // Convert, so by here there is one result for the lot. looping InputData again
1631 // copied that whole result over for every input there had been.
1632 foreach (var item in ConverterSettings.OutputClass)
1633 {
1634 if (null != addRowMethod) // GPALGrid
1635 {
1636 addRowMethod.Invoke(outputClass, new object[] { new List<string>() { (dynamic)item } });
1637 /* BUG - CAVEAT: duplicating data
1638 ConverterSettings.OutputData = inData;
1639 if (null != ConverterSettings.InDelimiter && null != ConverterSettings.OutDelimiter) // CAVEAT: BUG: NOTE: Should this be a list of delim?
1640 ConverterSettings.OutputData = inData.Replace(ConverterSettings.InDelimiter.Value, ConverterSettings.OutDelimiter.Value);
1641 string[] lines = ConverterSettings.OutputData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
1642
1643 // Enumerate over the lines using foreach loop
1644 foreach (object line in lines)
1645 addRowMethod.Invoke(outputClass, new object[] { new List<object>() { (dynamic)line } });
1646 */
1647 }
1648 }
1649 }
1650 else if (null != addMethod || null != enqueueMethod || null != pushMethod)
1651 {
1652 Type elementType = outputClass.GetType().IsGenericType ? outputClass.GetType().GetGenericArguments()[0] : null;
1653 if (elementType != null)
1654 {
1655 foreach (var compatibleItem in ConvertToCompatibleItems(ConverterSettings.OutputClass, elementType))
1656 {
1657 if (null != addMethod)
1658 addMethod.Invoke(outputClass, new object[] { compatibleItem });
1659 else if (null != enqueueMethod)
1660 enqueueMethod.Invoke(outputClass, new object[] { compatibleItem });
1661 else if (null != pushMethod)
1662 pushMethod.Invoke(outputClass, new object[] { compatibleItem });
1663 }
1664 }
1665 }
1666 else
1667 // we've already converted to this type, might that be the case for a gpal grid, too?
1668 outputClass = (T)(object)ConverterSettings.OutputClass;
1669
1670 return this;
1671 }
1672 }
1673 catch (Exception ex)
1674 {
1675 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Can't find 'Add/Enqueue/Push' method for [{ConverterSettings.OutputClassType}]", ConverterSettings, GPALObjectType.Other, ex);
1676 }
1677
1678 // not correct but what we are doing
1679 // ConverterSettings.OutputClass = ConverterHelper.CreateListFromClass<type>(ConverterSettings.InputDictionary);
1680 }
1681 else if (ConverterSettings.OutputClassType.IsGenericType && ConverterSettings.OutputClassType.GetGenericTypeDefinition() == typeof(List<>))
1682 {
1683 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1684 if (ConverterSettings.OutputClassElementType == typeof(string))
1685 {
1686 var resultList = new List<string>();
1687 foreach (var dict in ConverterSettings.InputDictionary)
1688 {
1689 // Build hierarchical strings with full paths
1690 resultList.AddRange(BuildHierarchicalStrings(dict));
1691 }
1692 ConverterSettings.OutputClass = resultList;
1693 ConverterSettings.OutputClass = (T)(object)resultList;
1694 }
1695 else
1696 {
1697 var mi = typeof(ConverterHelper).GetMethod("CreateListFromClass",
1698 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1699 null,
1700 new[] { typeof(ConverterSettings) },
1701 null);
1702 var fooRef = mi.MakeGenericMethod(new[] { ConverterSettings.OutputClassElementType });
1703 ConverterSettings.OutputClass = fooRef.Invoke(new ConverterHelper(), new object[] { ConverterSettings });
1704 }
1705 }
1706 else if (ConverterSettings.OutputClassType.IsArray && ConverterSettings.OutputClassType.GetElementType() == ConverterSettings.OutputClassElementType)
1707 {
1708 // It's an array of OutputClass[]
1709 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1710 if (ConverterSettings.OutputClassElementType == typeof(string))
1711 {
1712 var resultList = new List<string>();
1713 foreach (var dict in ConverterSettings.InputDictionary)
1714 {
1715 foreach (var kvp in dict)
1716 {
1717 resultList.Add(kvp.Value?.ToString() ?? string.Empty);
1718 }
1719 }
1720 ConverterSettings.OutputClass = resultList;
1721 }
1722 else
1723 {
1724 var mi = typeof(ConverterHelper).GetMethod("CreateIEnumerableFromClass",
1725 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1726 null,
1727 new[] { typeof(ConverterSettings) },
1728 null);
1729
1730 var fooRef = mi.MakeGenericMethod(new[] { ConverterSettings.OutputClassElementType });
1731 ConverterSettings.OutputClass = fooRef.Invoke(new ConverterHelper(), new dynamic[] { ConverterSettings });
1732 }
1733 }
1734 //else if (ConverterSettings.OutputClassType == typeof(string))
1735 //{
1736 // outputClass = (T)(object)ConverterSettings.InputData.ToString();
1737 //}
1738 else if (ConverterSettings.OutputClassType == ConverterSettings.OutputClassType) // to prevent last else from firing, just yet... how to check this tho...
1739 {
1740 // It's a single OutputClass
1741 if (0 < ConverterSettings.InputDictionary.Count)
1742 ConverterSettings.OutputClass = ConverterHelper.ConvertToClass(classInput, ConverterSettings.OutputClassType, classInput, ConverterSettings.ColumnNames?[0]); // NOTE: CAVEAT: hardcoded value
1743 // ConverterHelper.LoadClassFromDictionary(ConverterSettings.OutputClass, ConverterSettings.InputDictionary[0]);
1744 }
1745 else
1746 {
1747 // Unknown type
1748 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown type [{ConverterSettings.OutputClass}].", this, GPALObjectType.Converter);
1749 }
1750 } // end else (non-YAML path)
1751
1752 try
1753 {
1754 if (outputClass != ConverterSettings.OutputClass)
1755 outputClass = (T)ConverterSettings.OutputClass;
1756 } catch (Exception)
1757 {
1758 // outputClass may already be populated? so then return it without setting it
1759 }
1760 }
1761 else if (ConverterSettings.OutDataFormat == DataFormat.STRING)
1762 {
1763 outputClass = (T)(object)ConverterSettings.InputData;
1764 }
1765 #endregion <Output>
1766
1767 ResetIdleTimer();
1768
1769 if (null != ConverterSettings.InputFile)
1770 {
1771 ((IGPALFileInternal)ConverterSettings.InputFile).FileDictionary.Clear();
1772 ((IGPALFileInternal)ConverterSettings.InputFile).FileData.Clear();
1773 ((IGPALFileInternal)ConverterSettings.InputFile).FileGrid.Clear();
1774 ((IGPALFileInternal)ConverterSettings.InputFile).AlreadyTokenized = false;
1775 }
1776
1777 return this;
1778 }
1779
1787 {
1788 ConverterSettings.RegisteredStructs.Add(knownStructType);
1789 return this;
1790 }
1791
1792 // RenderTo is the presentation half of the converter, where SaveTo re-encodes losslessly and RenderTo
1793 // would produce a document the data cannot be read back out of: PDF, PNG, an ASCII or markdown table,
1794 // an HTML report. None of it is built. All four threw NotImplementedException, so a workflow could
1795 // write a chain that compiled and then fell over at run time. Commented out, along with their
1796 // declarations in Interfaces.cs, until there is something behind them.
1797 //
1798 // If it is ever built, the cheapest first step is already half written: GPAL.EmitTable and
1799 // EmitRuntimeTable render an ASCII table with the destination hardcoded to the console.
1800
1807 //public IAllowConverterInputAndActions RenderTo(IGPALDatabase outputDatabase)
1808 //{
1809 // throw new NotImplementedException();
1810 //}
1811
1818 //public IAllowConverterInputAndActions RenderTo(string outputData)
1819 //{
1820 // throw new NotImplementedException();
1821 //}
1822
1829 //public IAllowConverterInputAndActions RenderTo(GPALFile outputFile)
1830 //{
1831 // throw new NotImplementedException();
1832 //}
1833
1840 //public IAllowConverterInputAndActions RenderToClass(out dynamic outputClass) // NOTE: would this be a thing different from savetoclass?
1841 //{
1842 // throw new NotImplementedException();
1843 //}
1844 #region Helpers
1851 internal static DataFormat GetDataFormat(char delimiter)
1852 {
1853 switch (delimiter)
1854 {
1855 case ('^'):
1856 return DataFormat.CARET;
1857 case (':'):
1858 return DataFormat.COLON;
1859 case (','):
1860 return DataFormat.CSV;
1861 case ('.'):
1862 return DataFormat.DOT;
1863 case ('-'):
1864 return DataFormat.HYPHEN;
1865 case ('|'):
1866 return DataFormat.PIPE;
1867 case (' '):
1868 return DataFormat.SPACE;
1869 case (';'):
1870 return DataFormat.SEMICOLON;
1871 case ('\t'):
1872 return DataFormat.TAB;
1873 default:
1874 return DataFormat.CUSTOM_DELIMITER;
1875 }
1876 }
1877
1885 private List<string> BuildHierarchicalStrings(dynamic dictOrList, string prefix = "")
1886 {
1887 var result = new List<string>();
1888 if (dictOrList == null) return result;
1889
1890 // Handle dictionaries (like reporting, uses)
1891 if (ConverterHelper.IsDictionaryType(dictOrList.GetType()))
1892 {
1893 foreach (var kvp in dictOrList)
1894 {
1895 string key = kvp.Key.ToString();
1896 dynamic value = kvp.Value;
1897 string newPrefix = string.IsNullOrEmpty(prefix) ? "" : $"{prefix}.{key}";
1898
1899 if (ConverterHelper.IsDictionaryType(value.GetType()))
1900 {
1901 result.AddRange(BuildHierarchicalStrings(value));
1902 }
1903 else if (ConverterHelper.IsEnumerableType(value.GetType()) && value.GetType() != typeof(string))
1904 {
1905 int index = 0;
1906 foreach (var item in value)
1907 {
1908 string indexedPrefix = $"{newPrefix}[{index}]";
1909 result.AddRange(BuildHierarchicalStrings(item));
1910 index++;
1911 }
1912 }
1913 else
1914 {
1915 // Leaf node: add the key-value pair with the full path
1916 if (false == string.IsNullOrEmpty(newPrefix))
1917 result.Add($"{newPrefix}={value?.ToString() ?? ""}");
1918 else
1919 result.Add($"{value?.ToString() ?? ""}");
1920 }
1921 }
1922 }
1923 // Handle lists (like stored_procs)
1924 else if (ConverterHelper.IsEnumerableType(dictOrList.GetType()) && dictOrList.GetType() != typeof(string))
1925 {
1926 int index = 0;
1927 foreach (var item in dictOrList)
1928 {
1929 string indexedPrefix = $"{prefix}[{index}]";
1930 result.AddRange(BuildHierarchicalStrings(item, indexedPrefix));
1931 index++;
1932 }
1933 }
1934 else
1935 {
1936 // Leaf node: add the key-value pair with the full path
1937 result.Add($"{prefix}={dictOrList?.ToString() ?? ""}");
1938 }
1939
1940 return result;
1941 }
1949 private static IEnumerable<object> ConvertToCompatibleItems(object sourceItem, Type targetType)
1950 {
1951 var results = new List<object>();
1952
1953 if (sourceItem == null)
1954 return results;
1955
1956 // Direct compatibility check
1957 if (targetType.IsAssignableFrom(sourceItem.GetType()))
1958 {
1959 results.Add(sourceItem);
1960 return results;
1961 }
1962
1963 // Handle collections
1964 if (sourceItem is IEnumerable enumerable && !(sourceItem is string))
1965 {
1966 foreach (var nestedItem in enumerable)
1967 {
1968 results.AddRange(ConvertToCompatibleItems(nestedItem, targetType));
1969 }
1970 return results;
1971 }
1972
1973 // Handle dictionaries
1974 if (sourceItem is IDictionary dict)
1975 {
1976 foreach (var value in dict.Values)
1977 {
1978 if (value != null && targetType.IsAssignableFrom(value.GetType()))
1979 {
1980 results.Add(value);
1981 }
1982 else if (value != null)
1983 {
1984 results.AddRange(ConvertToCompatibleItems(value, targetType));
1985 }
1986 }
1987 return results;
1988 }
1989
1990 // Try type conversion
1991 var convertedItems = new List<object>();
1992 var converter = TypeDescriptor.GetConverter(targetType);
1993 if (converter != null && converter.CanConvertFrom(sourceItem.GetType()))
1994 {
1995 try
1996 {
1997 var converted = converter.ConvertFrom(sourceItem);
1998 convertedItems.Add(converted);
1999 }
2000 catch
2001 {
2002 // Skip items that can't be converted
2003 }
2004 }
2005 else if (targetType == typeof(string))
2006 {
2007 try
2008 {
2009 convertedItems.Add(sourceItem.ToString());
2010 }
2011 catch
2012 {
2013 // Skip items that can't be converted
2014 }
2015 }
2016
2017 results.AddRange(convertedItems);
2018 return results;
2019 }
2020
2021 internal static class DataFormatDetector
2022 {
2023 // Helper to detect the best DataFormat from input string.
2024 // Focuses on XML, JSON, YAML, CSV first, then others via heuristics.
2025 // Never throws - returns NOTSET on failure, publishes GPAL warnings.
2026 // Assumptions: Trims input, checks startsWith/patterns, tries light parsing.
2027 // For delimiters (CSV, TAB, etc.): Checks first few lines for consistent column count.
2028 // Add NuGets if needed: Newtonsoft.Json (JSON), YamlDotNet (YAML full parse - optional here).
2036 public static DataFormat DetectFormat(string input)
2037 {
2038 if (string.IsNullOrWhiteSpace(input))
2039 {
2040 GPAL.PublishSimpleEvent(
2041 GPALEventType.WARNING,
2042 "DetectFormat called with empty/null input. Returning NOTSET.",
2043 typeof(DataFormatDetector),
2044 GPALObjectType.Other);
2045
2046 return DataFormat.NOTSET;
2047 }
2048
2049 string trimmed = input.Trim();
2050
2051 // Early check: is this likely base64-encoded data?
2052 //if (IsLikelyBase64(trimmed))
2053 //{
2054 // return DataFormat.BASE64;
2055 //}
2056
2057 string firstLine = trimmed.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
2058
2059 // XML: Starts with <, valid tags (light check)
2060 if (trimmed.StartsWith("<") && IsLikelyXml(trimmed))
2061 {
2062 return DataFormat.XML;
2063 }
2064
2065 // JSON: Starts with { or [, try parse
2066 if ((trimmed.StartsWith("{") || trimmed.StartsWith("[")) && IsValidJson(trimmed))
2067 {
2068 return DataFormat.JSON;
2069 }
2070
2071 // YAML: Key: value patterns, no {/[ start, try basic check (full parse if YamlDotNet added)
2072 if (!trimmed.StartsWith("{") && !trimmed.StartsWith("[") && !trimmed.StartsWith("<") && IsLikelyYaml(trimmed))
2073 {
2074 return DataFormat.YAML;
2075 }
2076
2077 // HTML: Starts with <html> or <!DOCTYPE html> or has <body> etc.
2078 if (trimmed.StartsWith("<!DOCTYPE html>") || trimmed.StartsWith("<html") || Regex.IsMatch(trimmed, @"<body|</body|<head|</head", RegexOptions.IgnoreCase))
2079 {
2080 return DataFormat.HTML;
2081 }
2082
2085 //if (trimmed.StartsWith("%PDF-"))
2086 //{
2087 // return DataFormat.PDF;
2088 //}
2089
2090 // Tabular/Delimited: CSV, TAB, PIPE, SEMICOLON, SPACE/PRN, CARET, HYPHEN, COLON, DOT, CUSTOM_DELIMITER
2091 DataFormat delimFormat = DetectDelimiterFormat(trimmed);
2092 if (delimFormat != DataFormat.NOTSET)
2093 {
2094 return delimFormat;
2095 }
2096
2097 // Others: DATABASE (SQL dump?), GRID (maybe table-like), DICTIONARY (key=value?), CLASS (C# code?), STRING (fallback)
2098 // For now, fallback to STRING if none match
2099 if (Regex.IsMatch(firstLine, @"^\s*CREATE TABLE|SELECT|INSERT|UPDATE", RegexOptions.IgnoreCase))
2100 {
2101 return DataFormat.DATABASE;
2102 }
2103 else if (Regex.IsMatch(trimmed, @"class\s+\w+\s*{|public\s+class", RegexOptions.IgnoreCase))
2104 {
2105 return DataFormat.CLASS;
2106 }
2107 else if (IsKeyValueLike(trimmed, "="))
2108 {
2109 return DataFormat.DICTIONARY;
2110 }
2111
2112 GPAL.PublishSimpleEvent(
2113 GPALEventType.DEBUG,
2114 "No clear DataFormat detected for input starting with [" + firstLine.Substring(0, Math.Min(50, firstLine.Length)) + "]. Returning STRING.",
2115 input,
2116 GPALObjectType.Other);
2117
2118 return DataFormat.STRING;
2119 }
2120
2121 // Returns true if the string strongly resembles base64 encoding
2128 private static bool IsLikelyBase64(string s)
2129 {
2130 if (string.IsNullOrEmpty(s)) return false;
2131 if (s.Length < 12) return false; // too short to be useful base64
2132
2133 string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
2134
2135 // must contain only base64 characters
2136 foreach (char c in s)
2137 {
2138 if (alphabet.IndexOf(c) == -1)
2139 {
2140 return false;
2141 }
2142 }
2143
2144 // length should be multiple of 4 (or close, allowing for missing padding)
2145 int mod = s.Length % 4;
2146 if (mod != 0)
2147 {
2148 // accept common unpadded or incorrectly padded cases
2149 if (mod != 2 && mod != 0)
2150 {
2151 return false;
2152 }
2153 }
2154
2155 // rough entropy check: base64 is never all same character or very low variety
2156 // (this is optional but helps reject garbage)
2157 int unique = new HashSet<char>(s).Count;
2158 if (unique < 5 && s.Length > 20)
2159 {
2160 return false;
2161 }
2162
2163 return true;
2164 }
2170 private static bool IsLikelyXml(string input)
2171 {
2172 try
2173 {
2174 using (var reader = XmlReader.Create(new StringReader(input)))
2175 {
2176 while (reader.Read()) { } // Light parse
2177 }
2178 return true;
2179 }
2180 catch
2181 {
2182 return false;
2183 }
2184 }
2185
2191 private static bool IsValidJson(string input)
2192 {
2193 try
2194 {
2195 JsonConvert.DeserializeObject(input); // Or use System.Text.Json.JsonDocument.Parse(input)
2196 return true;
2197 }
2198 catch
2199 {
2200 return false;
2201 }
2202 }
2203
2210 private static bool IsLikelyYaml(string input)
2211 {
2212 // Basic heuristic: lines with key: value, indents, no XML/JSON starts
2213 // If YamlDotNet added: try YamlStream.Load(new StringReader(input))
2214 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
2215 int yamlPatterns = lines.Count(line => Regex.IsMatch(line.Trim(), @"^[\w\-]+:\s*(.+|$)") || line.StartsWith(" ") || line.StartsWith("- "));
2216 return yamlPatterns > 2 && lines.Length > 1; // Threshold
2217 }
2218
2226 private static DataFormat DetectDelimiterFormat(string input)
2227 {
2228 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
2229 .Take(5) // Check header + few rows
2230 .ToArray();
2231
2232 if (lines.Length < 2) return DataFormat.NOTSET; // Need at least header + data
2233
2234 // Common delimiters
2235 var delimiters = new Dictionary<char, DataFormat>
2236 {
2237 { ',', DataFormat.CSV },
2238 { '\t', DataFormat.TAB },
2239 { '|', DataFormat.PIPE },
2240 { ';', DataFormat.SEMICOLON },
2241 { ' ', DataFormat.SPACE }, // PRN is space-delimited
2242 { '^', DataFormat.CARET },
2243 { '-', DataFormat.HYPHEN },
2244 { ':', DataFormat.COLON },
2245 { '.', DataFormat.DOT }
2246 };
2247
2248 foreach (var kvp in delimiters)
2249 {
2250 if (IsConsistentDelimiter(lines, kvp.Key))
2251 {
2252 if (kvp.Key == ' ') return DataFormat.PRN; // Special for space
2253 return kvp.Value;
2254 }
2255 }
2256
2257 // Custom if mixed but consistent columns
2258 if (lines.Select(line => line.Split(new[] { ',', ';', '\t', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries).Length).Distinct().Count() == 1)
2259 {
2260 return DataFormat.CUSTOM_DELIMITER;
2261 }
2262
2263 // Grid if looks like fixed-width columns
2264 if (lines.All(line => line.Length == lines[0].Length) && Regex.IsMatch(lines[0], @"[a-zA-Z0-9]+\s+[a-zA-Z0-9]+"))
2265 {
2266 return DataFormat.GRID;
2267 }
2268
2269 return DataFormat.NOTSET;
2270 }
2271
2279 private static bool IsConsistentDelimiter(string[] lines, char delim)
2280 {
2281 var colCounts = lines.Select(line => line.Count(c => c == delim) + 1) // Columns = delim count + 1
2282 .ToArray();
2283
2284 // Consistent if all rows have same col count, and >1 cols, and not too few delims
2285 return colCounts.Distinct().Count() == 1 && colCounts[0] > 1 && colCounts[0] == colCounts.Max();
2286 }
2287
2295 private static bool IsKeyValueLike(string input, string separator)
2296 {
2297 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
2298 return lines.Count(line => line.Contains(separator) && line.Split(separator.ToCharArray()).Length == 2) > 2;
2299 }
2300 }
2301
2309 private string FormatPretty(string input)
2310 {
2311 if (string.IsNullOrWhiteSpace(input))
2312 return input;
2313
2314 var format = ConverterSettings.InDataFormat.FirstOrDefault();
2315
2316 try
2317 {
2318 switch (format)
2319 {
2320 case DataFormat.JSON:
2321 var jsonOptions = new System.Text.Json.JsonSerializerOptions
2322 {
2323 WriteIndented = true,
2324 Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
2325 };
2326 var jsonElement = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(input);
2327 return System.Text.Json.JsonSerializer.Serialize(jsonElement, jsonOptions);
2328
2329 case DataFormat.XML:
2330 var xmlDoc = System.Xml.Linq.XDocument.Parse(input, System.Xml.Linq.LoadOptions.PreserveWhitespace);
2331 return xmlDoc.ToString(System.Xml.Linq.SaveOptions.None);
2332
2333 case DataFormat.HTML:
2334 // Prefer HtmlAgilityPack for real HTML, fallback to XDocument
2335 try
2336 {
2337 var htmlDoc = new HtmlAgilityPack.HtmlDocument();
2338 htmlDoc.LoadHtml(input);
2339 return htmlDoc.DocumentNode.WriteContentTo(); // or .OuterHtml with formatting
2340 }
2341 catch
2342 {
2343 var xDoc = System.Xml.Linq.XDocument.Parse(input, System.Xml.Linq.LoadOptions.PreserveWhitespace);
2344 return xDoc.ToString(System.Xml.Linq.SaveOptions.None);
2345 }
2346
2347 case DataFormat.YAML:
2348 var deserializer = new DeserializerBuilder().Build();
2349 var serializer = new SerializerBuilder()
2350 .WithIndentedSequences()
2351 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
2352 .Build();
2353 var obj = deserializer.Deserialize<object>(input);
2354 return serializer.Serialize(obj);
2355
2356 // These formats cannot/should not be pretty-printed
2357 case DataFormat.CSV:
2358 case DataFormat.TAB:
2359 case DataFormat.LOG: // a finished line of text, written and read as it stands
2360 case DataFormat.PIPE:
2361 case DataFormat.COLON:
2362 case DataFormat.SEMICOLON:
2363 case DataFormat.SPACE:
2364 case DataFormat.HYPHEN:
2365 case DataFormat.CARET:
2366 case DataFormat.PRN:
2367 case DataFormat.BASE64:
2368 case DataFormat.DATABASE:
2369 case DataFormat.XLSX:
2371 case DataFormat.STRING:
2372 case DataFormat.CLASS:
2373 case DataFormat.DICTIONARY:
2374 case DataFormat.DOT:
2375 case DataFormat.GRID:
2376 return input;
2377
2378 default:
2379 return input;
2380 }
2381 }
2382 catch (Exception ex)
2383 {
2384 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"PrettyPrint failed for [{format}] [{ex.Message}]");
2385 return input; // safe fallback
2386 }
2387 }
2388 #endregion Helpers
2389 }
2390}
2391
2392
static int CountGraphNodes(object root, int limit, out bool cyclic)
Walks an object graph far enough to answer two questions about it: is it big, and does it hold a cycl...
IAllowConverterInput RegisterKnownStructType(Type knownStructType)
Registers a struct type as a "known" type for class/dictionary conversion, so the converter can corre...
IAllowConverterSettingsAndActions WithInput(dynamic inputClass)
Sets the input to an arbitrary class instance (or collection), clearing any previously configured inp...
IGPALConverter ToGPALObject()
Returns this converter cast to IGPALConverter to begin a fluent conversion chain.
IAllowConverterSettingsAndActions WithInput(IGPALDatabase inputDatabase)
Sets the input to the contents of a GPAL database, clearing any previously configured input/output se...
IAllowConverterSettingsAndActions WithInput(string inputData)
Sets the input to a raw string of data, clearing any previously configured input/output settings....
IAllowConverterSettingsAndActions WithColumnNames(List< string > columnNames)
Supplies the column names for this conversion. Caller-supplied names override the input source's own ...
IAllowConverterSettingsAndActions WithInput(GPALFile inputFile)
Sets the input to one or more files, clearing any previously configured input/output settings....
IAllowConverterSettingsAndActions WithInputType(DataFormat inDataFormat)
Explicitly overrides the detected/assumed input data format.
IAllowConverterSettingsAndActions WithColumnName(string columnName)
Appends a single column name to the conversion's column list. Repeated calls build the list in order....
IAllowConverterInputAndActions SaveTo(IGPALDatabase outputDatabase)
Converts the configured input to database format and writes the resulting rows into the given databas...
IAllowConverterSettingsAndActions WithIgnoreFirstLineColumnNames(bool ignoreFirstLine=true)
Specifies whether the first line/row of delimited input should be ignored (skipped) rather than treat...
IAllowConverterInputAndActions PrettyPrintTo(out string outputData)
Pretty-prints JSON, YAML, XML, or HTML. Other formats (CSV, TAB, PIPE, etc.) are returned as-is.
IAllowConverterInputAndActions AppendTo(GPALFile outputFile)
Converts the configured input and appends the resulting data to the given output file (instead of ove...
IAllowConverterSettingsAndActions WithInput(List< Dictionary< object, dynamic > > inputDictionary)
Sets the input to a list of dictionaries (one per input "document"), clearing any previously configur...
IAllowConverterSettingsAndActions WithColumnsEnclosedInQuotes(bool fieldsEnclosedInQuotes=true)
Specifies whether delimited fields should be wrapped in quotes when writing output.
IAllowConverterSettingsAndActions WithFirstLineHasColumnNames(bool firstLineHeaders=true)
Specifies whether the first line/row of delimited input contains column header names.
IAllowConverterInputAndActions SaveTo(GPALFile outputFile)
Converts the configured input and writes (overwrites) the resulting data to the given output file.
IAllowConverterSettingsAndActions WithInput(IGPALGrid< string > inputGrid)
Sets the input to a GPAL grid of strings (GRID format, comma input delimiter), clearing any previousl...
IAllowConverterInputAndActions SaveTo(ref IGPALGrid< string > outputGrid)
Converts the configured input to GRID format and writes the resulting rows/columns into the given gri...
IAllowConverterSettingsAndActions WithOutputType(DataFormat outDataFormat)
Explicitly sets the output data format to use when converting.
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
List< string > ReturnFilenames
Get the list of filenames saved to (returned).
Definition GPALFile.cs:550
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:468
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568