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
242 public IAllowConverterInputAndActions SaveTo(string outputData)
243 {
244 ConverterSettings.OutputFile = null;
245 ConverterSettings.OutputClass = null;
246 ConverterSettings.OutputClassType = null;
247 ConverterSettings.OutputClassElementType = null;
248 ConverterSettings.OutputGrid = null;
249
250 ConverterSettings.OutputData = outputData;
251 if (DataFormat.NOTSET == ConverterSettings.OutDataFormat)
252 ConverterSettings.OutDataFormat = DataFormat.STRING;
253 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{ConverterSettings.InDataFormat.FirstOrDefault()}] data as [{ConverterSettings.OutDataFormat}] to string.", this, GPALObjectType.Converter);
254 return Convert(ref outputData);
255 }
256
263 {
264 CancelIdleTimer();
265 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
266 ConverterSettings.ClearDerived();
267 ConverterSettings.InputClass = null;
268 ConverterSettings.InputClassType = null;
269 ConverterSettings.InputClassElementType = null;
270 ConverterSettings.InputFirstLineIsColumnHeaders = false;
271 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
272 ConverterSettings.InputFile = inputFile;
273 foreach (string filename in inputFile.Filenames)
274 ConverterSettings.InDataFormat.Add(ConverterHelper.GetDataFormatFromExtension(filename));
275 return this;
276 }
277
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, $"Appending [{ConverterSettings.InDataFormat.FirstOrDefault()}] data as [{ConverterSettings.OutDataFormat}] to [{outputFile.Filename}].", this, GPALObjectType.Converter);
287 return Convert(ref outputFile, true);
288 }
289
302 {
303 ConverterSettings.OutputFile = outputFile;
304 ConverterSettings.OutDataFormat = ConverterHelper.GetDataFormatFromExtension(outputFile.Filename); // will iterate files in convert and determine each dataformat
305 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{ConverterSettings.InDataFormat.FirstOrDefault()}] data as [{ConverterSettings.OutDataFormat}] to [{outputFile.Filename}].", this, GPALObjectType.Converter);
306 Convert(ref outputFile);
307 outputFile.ReturnFilenames.Add(outputFile.Filename); // return the filename we actually save to as a filename
308 return this;
309 }
310
317 {
318 CancelIdleTimer();
319 ConverterSettings.InDataFormat = new System.Collections.Generic.List<GenerallyPositive.Enums.DataFormat>();
320 ConverterSettings.ClearDerived();
321 ConverterSettings.InputFirstLineIsColumnHeaders = false;
322 ConverterSettings.ColumnNames = new GPALGrid<string>(); // new input starts a fresh column context; caller WithColumnName(s) set after this survive until the next WithInput
323 ConverterSettings.InputFile = null;
324 ConverterSettings.InputClass = inputClass;
325 ConverterSettings.InputClassType = inputClass.GetType();
326 ConverterSettings.InputClassElementType = ConverterSettings.InputClassType.GetElementType();
327 if (null == ConverterSettings.InputClassElementType && ConverterSettings.InputClassType.IsGenericType)
328 {
329 var genericArgs = ConverterSettings.InputClassType.GetGenericArguments();
330 ConverterSettings.InputClassElementType = ConverterHelper.IsDictionaryType(ConverterSettings.InputClassType) && genericArgs.Length > 1
331 ? genericArgs[1] // Use value type for dictionaries (e.g., SelectorSet)
332 : genericArgs.FirstOrDefault();
333 }
334 HashSet<object> visited = new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
335
336 ConverterHelper.ConverterSettings = ConverterSettings;
337 ConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> { ConverterHelper.ConvertClassToDictionary(inputClass, null != ConverterSettings.InputClassElementType ? ConverterSettings.InputClassElementType.ToString() : ConverterSettings.InputClassType.ToString(), visited) };
338 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out IGPALGrid<string> outGrid));
339 ConverterSettings.InDataFormat.Add(DataFormat.CLASS);
340 ConverterSettings.InputGrid.Add(outGrid);
341
342 return this;
343 }
344 public IAllowConverterInputAndActions SaveTo<T>(ref T outputClass)
345 {
346 ConverterSettings.OutputFile = null;
347 ConverterSettings.OutputClassType = outputClass.GetType();
348 ConverterSettings.OutputClassElementType = ConverterSettings.OutputClassType.GetElementType();
349
350 if (ConverterSettings.OutputClassElementType == null)
351 {
352 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
353
354 if (genericArgs.Length > 0)
355 {
356 // Check if it's a Dictionary<TKey, TValue>
357 if (ConverterHelper.IsDictionaryType(ConverterSettings.OutputClassType))
358 {
359 ConverterSettings.OutputClassElementType = genericArgs[1]; // TValue
360 }
361 // Check if it's any IGPALGrid<T>
362 else if (ConverterSettings.OutputClassType.IsGenericType &&
363 ConverterSettings.OutputClassType.GetGenericTypeDefinition() == typeof(IGPALGrid<>))
364 {
365 // For IGPALGrid<T>, the "element" type is List<T>
366 var elementT = genericArgs[0]; // T
367 ConverterSettings.OutputClassElementType = typeof(List<>).MakeGenericType(elementT);
368 }
369 // Fallback: assume it's IEnumerable<T> or similar
370 else
371 {
372 ConverterSettings.OutputClassElementType = genericArgs[0];
373 }
374 }
375 }
376
377 ConverterSettings.OutDataFormat = DataFormat.CLASS;
378 Convert(ref outputClass);
379
380 return this;
381 }
387 public IAllowConverterSettingsAndActions WithInputType(DataFormat inDataFormat)
388 {
389 // Every .WithInput leaves one entry per real input, so overriding means replacing the last of them.
390 // Appending made Convert believe there was an extra input, which generated a phantom output file and
391 // then indexed past the inputs that actually exist.
392 // The empty case is index safety only. Reaching it means this was called before any .WithInput, and
393 // .WithInput starts a fresh list, so the value is discarded either way.
394 if (0 == ConverterSettings.InDataFormat.Count)
395 ConverterSettings.InDataFormat.Add(inDataFormat);
396 else
397 ConverterSettings.InDataFormat[ConverterSettings.InDataFormat.Count - 1] = inDataFormat;
398
399 return this;
400 }
401
406 public IAllowConverterSettingsAndActions WithOutputType(DataFormat outDataFormat)
407 {
408 ConverterSettings.OutDataFormat = outDataFormat;
409 return this;
410 }
411
417 {
418 ConverterSettings.FirstLineIsColumnHeaders = firstLineHeaders;
419 ConverterSettings.InputFirstLineIsColumnHeaders = firstLineHeaders;
420 return this;
421 }
422
428 {
429 ConverterSettings.IgnoreFirstLineColumnHeaders = ignoreFirstLine;
430 return this;
431 }
432
437 public IAllowConverterSettingsAndActions WithColumnsEnclosedInQuotes(bool fieldsEnclosedInQuotes = true)
438 {
439 ConverterSettings.FieldsEnclosedInQuotes = fieldsEnclosedInQuotes;
440 return this;
441 }
442
451 public IAllowConverterSettingsAndActions WithColumnNames(List<string> columnNames)
452 {
453 ConverterSettings.ColumnNames = new GPALGrid<string>();
454 if (null != columnNames && 0 < columnNames.Count)
455 ConverterSettings.ColumnNames.AddRow(new List<string>(columnNames));
456 return this;
457 }
458
466 {
467 if (0 == ConverterSettings.ColumnNames.Count())
468 ConverterSettings.ColumnNames.AddRow(new List<string>());
469 ConverterSettings.ColumnNames[0].Add(columnName);
470 return this;
471 }
472
480 private List<string> ResolveEffectiveColumns(List<string> sourceColumns)
481 {
482 List<string> caller = (null != ConverterSettings.ColumnNames && 0 < ConverterSettings.ColumnNames.Count())
483 ? ConverterSettings.ColumnNames[0]
484 : null;
485 if (null == caller || 0 == caller.Count)
486 return (null != sourceColumns) ? new List<string>(sourceColumns) : new List<string>();
487
488 List<string> effective = new List<string>(caller);
489 if (null != sourceColumns)
490 for (int i = effective.Count; i < sourceColumns.Count; i++)
491 effective.Add(sourceColumns[i]);
492 return effective;
493 }
494
503 public IAllowConverterInputAndActions PrettyPrintTo(out string outputData)
504 {
505 ConverterSettings.OutputFile = null;
506 ConverterSettings.OutputClass = null;
507 ConverterSettings.OutputClassType = null;
508 ConverterSettings.OutputClassElementType = null;
509 ConverterSettings.OutputGrid = null;
510
511 // Use the Input format (already known from WithInput) for pretty printing
512 if (ConverterSettings.InDataFormat.FirstOrDefault() != DataFormat.NOTSET)
513 ConverterSettings.OutDataFormat = ConverterSettings.InDataFormat.FirstOrDefault();
514
515 outputData = FormatPretty(ConverterSettings.InputData.FirstOrDefault());
516 ConverterSettings.OutputData = outputData;
517
518 return this;
519 }
520
521 #endregion Fluent Interface
522
533 int InputWithin(int inputIdx, int inputCount, string whatIsMissing)
534 {
535 int within = inputIdx;
536
537 if (inputCount <= inputIdx)
538 {
539 within = inputCount - 1;
540 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Output [{inputIdx + 1}] has no {whatIsMissing} of its own, only [{inputCount}] given. Using the last one.", this, GPALObjectType.Converter);
541 }
542
543 return within;
544 }
545
546 internal IAllowConverterInputAndActions Convert<T>(ref T outputClass, bool appendToFile = false)
547 {
548 CancelIdleTimer();
549 ConverterHelper.ConverterSettings = ConverterSettings;
550
551 #region <Input>
552 IGPALGrid<string> outGrid = GPAL.Grid.ToGPALObject();
553 ConverterSettings.OutputData = null;
554 // NOTE: ColumnNames is NOT cleared here. It is reset per WithInput and may hold caller-supplied
555 // names (WithColumnName/s) that must survive across chained SaveTo calls; the source-derived
556 // fallbacks below only fill it when the caller left it empty.
557
558 if (null != ConverterSettings.InputFile)
559 {
560 if (true == File.Exists(ConverterSettings.InputFile))
561 {
562 if (false == FileHelper.TokenizeFile(ConverterSettings.InputFile, ConverterSettings))
563 {
564 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to tokenize file [{ConverterSettings.InputFile.Filename}]. Continuing.", ConverterSettings, GPALObjectType.Other);
565 return this;
566 }
567 ConverterSettings.InputDictionary = ((IGPALFileInternal)ConverterSettings.InputFile).FileDictionary; // CAVEAT: what about multiple files in the GPALFile?
568 ConverterSettings.InputData = ((IGPALFileInternal)ConverterSettings.InputFile).FileData;
569 ConverterSettings.InputGrid = ((IGPALFileInternal)ConverterSettings.InputFile).FileGrid;
570 // Only fall back to the file's own column list when the caller did not supply columns,
571 // and copy it (never alias) so the file's ColumnList is never mutated by this conversion.
572 if (ConverterHelper.IsDelimitedDataFormat(ConverterHelper.GetDataFormatFromExtension(ConverterSettings.InputFile.Filename))
573 && 0 == ConverterSettings.ColumnNames.Count())
574 {
575 ConverterSettings.ColumnNames = new GPALGrid<string>();
576 foreach (List<string> colRow in ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList)
577 ConverterSettings.ColumnNames.AddRow(new List<string>(colRow));
578 }
579 ConverterSettings.ExcelRowsPerSheet = ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ExcelRowsPerSheet;
580 ConverterSettings.ExcelSheetNames = ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ExcelSheetNames;
581 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
582 ConverterSettings.InDelimiter = ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter[0]; // default for non-file output paths; overridden per input file below
583
584 // InDataFormat needs no repopulating here. It is filled by WithInput, one entry per input, and
585 // nothing empties it afterwards: the idle timer's ClearDerived drops the cached payloads, not
586 // the declared formats. So it survives every SaveTo/AppendTo in a chain on its own.
587 }
588 else
589 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"File missing: [{ConverterSettings.InputFile.Filename}]", ConverterSettings, GPALObjectType.Other);
590 }
591 else if (0 < ConverterSettings.InputData.Count) // we were given custom input data or type
592 {
593 for (int idx = 0; idx < ConverterSettings.InputData.Count; idx++)
594 {
595 switch (ConverterSettings.InDataFormat[idx])
596 {
597 case DataFormat.CUSTOM_DELIMITER:
598 case DataFormat.CARET:
599 case DataFormat.CSV:
600 case DataFormat.LOG: // a finished line of text, written and read as it stands
601 case DataFormat.PIPE:
602 case DataFormat.PRN:
603 case DataFormat.TAB:
604 ConverterSettings.InputDictionary.Clear();
605 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertDelimitedToDictionary(ConverterSettings, new StringReader(ConverterSettings.InputData[idx])));
606 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
607 ConverterSettings.InputGrid.Add(outGrid);
608 break;
609 case DataFormat.STRING:
610 ConverterSettings.InputGrid.Add(GPAL.Grid.AddRow(new List<string> { ConverterSettings.InputData[idx] }).ToGPALObject());
611 ConverterSettings.InputDictionary.Clear();
612 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertDelimitedToDictionary(ConverterSettings, new StringReader(ConverterSettings.InputData[idx])));
613 break;
614 case DataFormat.DICTIONARY:
615 break;
616 case DataFormat.JSON:
617 var desObj = JsonConvert.DeserializeObject<dynamic>(ConverterSettings.InputData[idx], new JsonConverter[] { new CustomJsonConverter() });
618 if (null != desObj)
619 {
620 Dictionary<object, dynamic> jsonDict;
621 if (desObj is IDictionary)
622 {
623 jsonDict = (Dictionary<object, dynamic>)desObj;
624 }
625 else if (desObj is IList jsonList)
626 {
627 // Root is a JSON array, not an object - synthesize row keys
628 // (same GPALKEYN convention used for headerless CSV rows and YAML sequence roots).
629 jsonDict = new Dictionary<object, dynamic>();
630 int rootIdx = 0;
631 foreach (var item in jsonList)
632 {
633 jsonDict[$"GPALKEY{rootIdx++:D4}"] = item;
634 }
635 }
636 else
637 {
638 // Root is a bare JSON scalar.
639 jsonDict = new Dictionary<object, dynamic> { ["GPALKEY0000"] = desObj };
640 }
641
642 ConverterSettings.InputDictionary.Add(jsonDict);
643 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
644 ConverterSettings.InputGrid.Add(outGrid);
645 }
646 else
647 return this;
648
649 break;
650 case DataFormat.PDF:
651 break;
652 case DataFormat.XLSX:
653 break;
654 case DataFormat.XML:
655 ConverterSettings.InputXmlDocument.PreserveWhitespace = true;
656 ConverterSettings.InputXmlDocument.Load(ConverterSettings.InputData[idx]);
657 ConverterSettings.InputDictionary.Add(ConverterHelper.ConvertXMLToDictionary(ConverterSettings.InputXmlDocument));
658 ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid);
659 ConverterSettings.InputGrid.Add(outGrid);
660 break;
661 case DataFormat.YAML:
662 var deserializer = new DeserializerBuilder()
663 .WithNamingConvention(CamelCaseNamingConvention.Instance)
664 .Build();
665
666 dynamic yamlObject = deserializer.Deserialize<object>(ConverterSettings.InputData[idx]);
667 var yamlGridRows = new List<List<string>>();
668 var processedYamlDict = new Dictionary<dynamic, dynamic>();
669
670 if (yamlObject is IDictionary yamlMapping)
671 {
672 foreach (var key in yamlMapping.Keys)
673 {
674 string keyStr = key?.ToString() ?? "null_key";
675 processedYamlDict[keyStr] = FileHelper.ProcessDictionaryValue(yamlMapping[key], ConverterSettings, yamlGridRows);
676 }
677 }
678 else if (yamlObject is IList yamlSequence)
679 {
680 // Root is a YAML sequence, not a mapping - synthesize row keys
681 // (same GPALKEYN convention used for headerless CSV rows).
682 int rootIdx = 0;
683 foreach (var item in yamlSequence)
684 {
685 processedYamlDict[$"GPALKEY{rootIdx++:D4}"] = FileHelper.ProcessDictionaryValue(item, ConverterSettings, yamlGridRows);
686 }
687 }
688 else
689 {
690 // Root is a bare YAML scalar.
691 processedYamlDict["GPALKEY0000"] = FileHelper.ProcessDictionaryValue(yamlObject, ConverterSettings, yamlGridRows);
692 }
693
694 var yamlGridRow = new GPALGrid<string>();
695 foreach (var row in yamlGridRows)
696 {
697 yamlGridRow.AddRow(row);
698 }
699
700 ConverterSettings.InputDictionary.Add(processedYamlDict);
701 ConverterSettings.InputGrid.Add(yamlGridRow);
702 break;
703 case DataFormat.HTML:
704 ConverterSettings.InputDictionary = ConverterHelper.ConvertHtmlToDictionary(ConverterSettings.InputData[idx]);
705 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid));
706 ConverterSettings.InputGrid.Add(outGrid);
707 break;
708 }
709 }
710 }
711 else if (null != ConverterSettings.InputDatabase)
712 {
713 List<string> dbColumns = new List<string>();
714
715 // TODO: convert to other types
716 DatabaseHelper.TokenizeDatabase(null, ConverterSettings.InputDatabase);
717 ConverterSettings.InputGrid.Add(ConverterSettings.InputDatabase.Tokens);
718 if (0 < ((GPALDatabase)ConverterSettings.InputDatabase).DatabaseSettings.ColumnList.Count) // no columns means no results
719 foreach (DatabaseSettings.DatabaseColumn column in ((GPALDatabase)ConverterSettings.InputDatabase).DatabaseSettings.ColumnList)
720 dbColumns.Add(column.ColumName);
721
722 // Caller-supplied columns override the database's own columns positionally; the database is
723 // just the fallback/backfill, exactly like a file's column list.
724 List<string> columns = ResolveEffectiveColumns(dbColumns);
725 if (0 == ConverterSettings.ColumnNames.Count() && 0 < columns.Count)
726 ConverterSettings.ColumnNames.AddRow(new List<string>(columns));
727
728 ConverterSettings.InputDictionary = ConverterHelper.ConvertGridToDictionary(columns, ConverterSettings.InputDatabase.Tokens);
729 }
730 else if (null != ConverterSettings.InputGrid && 0 < ConverterSettings.InputGrid.Count)
731 {
732 // Caller-supplied columns key the rows. For a grid the "source" names are the browser's
733 // selector names, which the converter cannot see, so the browser tops HeaderList up from
734 // selector names before handing the grid over; any names still missing here are generated.
735 List<string> columns = ResolveEffectiveColumns(null);
736 bool haveColumns = 0 < columns.Count;
737
738 ConverterSettings.InputDictionary.Clear();
739 foreach (IGPALGrid<string> inGrid in ConverterSettings.InputGrid)
740 {
741 // One dictionary per grid (one grid == one input source), rows keyed under GPALKEY - the
742 // same shape delimited-file input produces - keyed by column name when columns are known.
743 Dictionary<object, dynamic> source = new Dictionary<object, dynamic>();
744 int lineNumber = 0;
745 foreach (List<string> gridRow in inGrid)
746 {
747 if (haveColumns)
748 {
749 Dictionary<string, string> rowDict = new Dictionary<string, string>();
750 for (int i = 0; i < gridRow.Count; i++)
751 rowDict[i < columns.Count ? columns[i] : $"Column{i + 1}"] = gridRow[i];
752 source[$"GPALKEY{lineNumber:D4}"] = rowDict;
753 }
754 else
755 source[$"GPALKEY{lineNumber:D4}"] = new List<string>(gridRow);
756 lineNumber++;
757 }
758 ConverterSettings.InputDictionary.Add(source);
759 }
760 }
761 else if (null != ConverterSettings.InputClass && 0 < ConverterSettings.InDataFormat.Count)
762 {
763 // Re-derive from persisted InputClass for chained SaveTo calls (same role as InputFile re-tokenization above)
764 HashSet<object> visited = new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
765 IGPALGrid<string> repopGrid;
766 ConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> {
767 ConverterHelper.ConvertClassToDictionary(
768 ConverterSettings.InputClass,
769 null != ConverterSettings.InputClassElementType
770 ? ConverterSettings.InputClassElementType.ToString()
771 : ConverterSettings.InputClassType.ToString(),
772 visited)
773 };
774 ConverterSettings.InputData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out repopGrid));
775 ConverterSettings.InDataFormat.Add(DataFormat.CLASS);
776 ConverterSettings.InputGrid.Add(repopGrid);
777 }
778 #endregion <Input>
779 #region <Output>
780 StringBuilder stringBuilder = new StringBuilder();
781 if (null != ConverterSettings.OutputFile)
782 {
783 // Pair each output file with an input "dataset" (one per input file, or one for class/grid/database/etc input):
784 // output[i] is converted from input[min(i, inputFileCount-1)] - the last input services any extra outputs.
785 // If there are more inputs than outputs, grow the output filename list using the last user-supplied
786 // output filename as a naming pattern (name_2.ext, name_3.ext, ...).
787 int inputFileCount = Math.Max(1, ConverterSettings.InDataFormat.Count);
788 GPALFileSettings outputFileSettings = ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings;
789
790 // worked out fresh for this conversion and never written back. Filenames holds what the workflow
791 // supplied and nothing else, so the pattern is always a real filename rather than one of these,
792 // and a GPALFile reused across conversions does not grow a longer list every time
793 List<string> outputPaths = new List<string>(outputFileSettings.Filenames);
794
795 if (0 < outputPaths.Count && outputPaths.Count < inputFileCount)
796 {
797 string pattern = outputPaths[outputPaths.Count - 1];
798 string patternDirectory = Path.GetDirectoryName(pattern);
799 string patternName = Path.GetFileNameWithoutExtension(pattern);
800 string patternExtension = Path.GetExtension(pattern);
801
802 for (int newFileIdx = outputPaths.Count; newFileIdx < inputFileCount; newFileIdx++)
803 {
804 string generatedFilename = Path.Combine(patternDirectory, $"{patternName}_{newFileIdx + 1}{patternExtension}");
805 outputPaths.Add(generatedFilename);
806 ConverterSettings.OutputFile.ReturnFilenames.Add(generatedFilename);
807 }
808 }
809
810 int fileIdx = 0;
811 DataFormat savedOutDataFormat = ConverterSettings.OutDataFormat;
812 foreach (string filename in outputPaths)
813 {
814 int inputIdx = Math.Min(fileIdx, inputFileCount - 1);
815
816 // each input file may have its own delimiter (e.g. mixing .csv and .tab inputs) - use the one paired with this output
817 if (null != ConverterSettings.InputFile && inputIdx < ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter.Count)
818 ConverterSettings.InDelimiter = ((IGPALFileInternal)ConverterSettings.InputFile).Delimiter[inputIdx];
819
820 // 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
821 if (DataFormat.NOTSET == savedOutDataFormat)
822 ConverterSettings.OutDataFormat = ConverterHelper.GetDataFormatFromExtension(filename);
823
824 /* - not sure, assume the output delim is the same as input? seems outdated logic
825 if (0 < ConverterSettings.OutputFile.FileSettings.Delimiter.Count() && true == ConverterSettings.OutputFile.FileSettings.Delimiter[fileIdx].HasValue)
826 ConverterSettings.OutDelimiter = ConverterSettings.OutputFile.FileSettings.Delimiter[fileIdx].Value;
827 else
828 */
829 if (fileIdx < ((IGPALFileInternal)ConverterSettings.OutputFile).FirstLineIsColumnNames.Count)
830 // An explicit output-file setting (true OR false) always wins - this is how the user
831 // turns the header row off despite having named columns.
832 ConverterSettings.FirstLineIsColumnHeaders = ((IGPALFileInternal)ConverterSettings.OutputFile).FirstLineIsColumnNames[fileIdx];
833 else
834 // No explicit setting: emit a header when we have column names to write (named via
835 // WithHeader / WithColumnName(s), or resolved from the source), or when the caller
836 // declared headers via WithFirstLineHasColumnNames. Naming columns implies wanting them.
837 ConverterSettings.FirstLineIsColumnHeaders =
838 (null != ConverterSettings.ColumnNames && 0 < ConverterSettings.ColumnNames.Count())
839 || ConverterSettings.InputFirstLineIsColumnHeaders;
840
841 // Headers are never repeated when appending to an existing file.
842 if (true == appendToFile)
843 ConverterSettings.FirstLineIsColumnHeaders = false;
844
845 if (fileIdx < ((IGPALFileInternal)ConverterSettings.OutputFile).FieldsEnclosedInQuotes.Count)
846 ConverterSettings.FieldsEnclosedInQuotes = ((IGPALFileInternal)ConverterSettings.OutputFile).FieldsEnclosedInQuotes[fileIdx];
847
848 if (null == ConverterSettings.OutDelimiter)
849 ConverterSettings.OutDelimiter = ConverterHelper.GetDelimiterFromFormat(ConverterSettings.OutDataFormat);
850
851 if (null != ConverterSettings.ColumnNames && 0 == ConverterSettings.ColumnNames.Count())
852 {
853 List<string> columns = new List<string>();
854
855 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())
856 foreach (string columnName in ((IGPALFileInternal)ConverterSettings.OutputFile).FileSettings.ColumnList[fileIdx])
857 columns.Add(columnName);
858 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())
859 foreach (string columnName in ((IGPALFileInternal)ConverterSettings.InputFile).FileSettings.ColumnList[inputIdx])
860 columns.Add(columnName);
861
862 if (0 < columns.Count)
863 ConverterSettings.ColumnNames.AddRow(columns);
864 }
865
866 switch (ConverterSettings.OutDataFormat)
867 {
868 case DataFormat.CUSTOM_DELIMITER:
869 case DataFormat.CARET:
870 case DataFormat.CSV:
871 case DataFormat.LOG: // a finished line of text, written and read as it stands
872 case DataFormat.PIPE:
873 case DataFormat.PRN:
874 case DataFormat.TAB:
875 case DataFormat.NOTSET:
876 if (0 < ConverterSettings.InDataFormat.Count)
877 {
878 switch (ConverterSettings.InDataFormat[inputIdx])
879 {
880 case DataFormat.CARET:
881 case DataFormat.COLON:
882 case DataFormat.CSV:
883 case DataFormat.CUSTOM_DELIMITER:
884 case DataFormat.DOT:
885 //case DataFormat.HTML:
886 case DataFormat.HYPHEN:
887 case DataFormat.LOG: // a finished line of text, written and read as it stands
888 case DataFormat.PIPE:
889 case DataFormat.SEMICOLON:
890 case DataFormat.SPACE:
891 case DataFormat.TAB:
892 case DataFormat.PRN:
893 if (inputIdx < ConverterSettings.InputData.Count)
894 ConverterSettings.OutputData = ConverterSettings.InputData[inputIdx];
895 else
896 ConverterSettings.OutputData = File.ReadAllText(ConverterSettings.InputFile.Filenames[inputIdx]);
897 if (null != ConverterSettings.OutputData && null != ConverterSettings.InDelimiter && true == ConverterSettings.InDelimiter.HasValue)
898 {
899 using (StringReader reader = new StringReader(ConverterSettings.OutputData))
900 {
901 string line;
902 StringBuilder lineBuilder = new StringBuilder();
903 while ((line = reader.ReadLine()) != null)
904 {
905 string[] tokens = line.Split(ConverterSettings.InDelimiter.Value);
906 foreach (string str in tokens)
907 stringBuilder.Append(ConverterHelper.CreateCSVToken(str, ConverterSettings)); // if these should be wrapped in "", createcsvtoken will do that
908 if (0 < stringBuilder.Length)
909 stringBuilder.Remove(stringBuilder.Length - 1, 1); // remove trailing inDelimiter
910 lineBuilder.AppendLine(stringBuilder.ToString());
911 stringBuilder.Clear();
912 }
913 ConverterSettings.OutputData = lineBuilder.ToString();
914 }
915 }
916 stringBuilder.Append(ConverterSettings.OutputData);
917 break;
918 case DataFormat.HTML:
919 //stringBuilder.Append(ConverterSettings.InputData[fileIdx]); // NOTE: this feels like the wrong variable name
920 //break;
921 case DataFormat.CLASS:
922 default: // nested structure? use dictionary - maybe we can always use the dictionary?
923 stringBuilder.Append(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings, out outGrid));
924 break;
925 }
926 if (true == appendToFile)
927 File.AppendAllText(filename, stringBuilder.ToString());
928 else
929 File.WriteAllText(filename, stringBuilder.ToString());
930 }
931 else // not sure this will ever hit...
932 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"File [{ConverterSettings.InputFile}] does not exist(?) as no DATA FORMAT has been set.", this, GPALObjectType.Converter);
933 break;
934 case DataFormat.JSON:
935 if (ConverterSettings.InputDictionary.Count > 0)
936 {
937 int dictIdx = InputWithin(inputIdx, ConverterSettings.InputDictionary.Count, "input");
938 var rootDict = ConverterSettings.InputDictionary[dictIdx] as IDictionary;
939
940 object rawObject;
941
942 if (rootDict != null && ConverterHelper.IsArrayLike(rootDict))
943 {
944 var list = new List<object>();
945 foreach (DictionaryEntry e in rootDict)
946 {
947 string k = e.Key.ToString();
948 if (k.StartsWith("GPALKEY"))
949 {
950 list.Add(e.Value);
951 }
952 }
953 rawObject = list;
954 }
955 else
956 {
957 rawObject = rootDict ?? ConverterSettings.InputDictionary[dictIdx];
958 }
959
960 dynamic cleanObject = ConverterHelper.NormalizeForCleanJson(rawObject);
961 cleanObject = ConverterHelper.StripGpalKeys(cleanObject);
962
963 var settings = new JsonSerializerSettings
964 {
965 // Your existing settings, e.g. NamingStrategy if needed
966 Converters =
967 {
968 new IPAddressJsonConverter(),
969 new ComplexJsonConverter(),
970 new UriJsonConverter(),
971 new VersionJsonConverter(),
972 // new BigIntegerJsonConverter(), // only if needed
973 // Add any others you already have (e.g. for quoted strings)
974 },
975 ReferenceLoopHandling = ReferenceLoopHandling.Ignore, // ignore self referencing classes/structs as an error
976 Formatting = Newtonsoft.Json.Formatting.Indented // optional, for readable output
977 };
978
979 ConverterSettings.OutputData = JsonConvert.SerializeObject(cleanObject, settings);
980
981 if (appendToFile)
982 File.AppendAllText(filename, ConverterSettings.OutputData);
983 else
984 File.WriteAllText(filename, ConverterSettings.OutputData);
985 }
986 else
987 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
988 break;
989 case DataFormat.PDF:
990 break;
991 case DataFormat.XLSX:
992 if (inputIdx < ConverterSettings.InputGrid.Count)
993 ExcelHelper.WriteXlsxFromTokenizedData(
994 outputPath: filename,
995 grid: ConverterSettings.InputGrid[inputIdx], // clone if you don't want the helper to have a reference to your live grid
996 rowsPerSheet: ConverterSettings.ExcelRowsPerSheet[inputIdx],
997 columnNamesPerSheet: ConverterSettings.ColumnNames,
998 sheetNames: ConverterSettings.ExcelSheetNames[inputIdx],
999 writeHeaders: ConverterSettings.FirstLineIsColumnHeaders // or true if you always want headers
1000 );
1001 else
1002 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1003 break;
1004 case DataFormat.XML:
1005 if (0 < ConverterSettings.InputDictionary.Count)
1006 {
1007 ConverterSettings.OutputXmlDocument = new XmlDocument();
1008
1009 // Convert the dictionary to XML using recursion
1010 var rootElement = ConverterSettings.OutputXmlDocument.CreateElement("root");
1011 ConverterSettings.OutputXmlDocument.AppendChild(rootElement);
1012
1013 // get existing file, find root, then start adding to it
1014 if (true == appendToFile)
1015 {
1016 XmlDocument xmlDocument = new XmlDocument();
1017 try
1018 {
1019 xmlDocument.Load(filename);
1020 ConverterSettings.OutputXmlDocument = xmlDocument;
1021 rootElement = (XmlElement)xmlDocument.GetElementsByTagName("root")[0];
1022 }
1023 catch (Exception ex)
1024 {
1025 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"File [{filename}] does not exist to append XML to, writing new.", this, GPALObjectType.Converter, ex);
1026 }
1027 }
1028
1029 int dictCount = 0;
1030 foreach (Dictionary<object, dynamic> dict in ConverterSettings.InputDictionary)
1031 {
1032 if (DataFormat.HTML == ConverterSettings.InDataFormat[dictCount])
1033 ConverterHelper.ConvertHtmlDictionaryToXml(dict, rootElement); // append to rootElement in method
1034 else if (DataFormat.XML == ConverterSettings.InDataFormat[dictCount])
1035 ConverterHelper.ConvertDictionaryToXmlElement(dict, rootElement, false); // append to rootElement in method
1036 else
1037 ConverterHelper.ConvertDictionaryToXmlElement(dict, rootElement); // append to rootElement in method
1038 dictCount++;
1039 }
1040
1041 // NOTE: Preserve original XML header(s)
1042 string inputFilename = ConverterSettings.InputFile?.Filename; // the original source file
1043
1044 if (true == ConverterSettings.InputFile?.Filename.ToLower().EndsWith("xml"))
1045 {
1046 string header = ConverterHelper.GetXmlHeaderFromFile(inputFilename);
1047
1048 // Write header from original document
1049 if (0 < header.Length)
1050 using (var writer = new StreamWriter(filename, append: false))
1051 {
1052 writer.Write(header);
1053
1054 using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings
1055 {
1056 OmitXmlDeclaration = true,
1057 Indent = true,
1058 Encoding = writer.Encoding
1059 }))
1060 {
1061 ConverterSettings.OutputXmlDocument.Save(xmlWriter);
1062 }
1063 }
1064 else
1065 ConverterSettings.OutputXmlDocument.Save(filename);
1066 }
1067 else
1068 ConverterSettings.OutputXmlDocument.Save(filename);
1069 }
1070 break;
1071 case DataFormat.YAML:
1072 if (ConverterSettings.InputClass != null)
1073 {
1074 var serializer = new SerializerBuilder()
1075 .WithObjectGraphTraversalStrategyFactory((ti, tr, tc, depth)
1076 => new FullObjectGraphTraversalStrategy(ti, tr, depth, CamelCaseNamingConvention.Instance, new DefaultObjectFactory()))
1077 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1078 .WithEventEmitter(next => new ExplicitNullYamlEventEmitter(next))
1079 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
1080 .WithTypeConverter(new IPAddressYamlConverter())
1081 .WithTypeConverter(new ComplexYamlConverter())
1082 .WithTypeConverter(new UriYamlConverter())
1083 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1084 .WithTypeConverter(new NullableGuidYamlConverter())
1085 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1086 .WithTypeConverter(new VersionYamlConverter())
1087 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1088 .WithTypeConverter(new QuotedStringYamlConverter())
1089 .WithTypeConverter(new WaitTimeYamlConverter())
1090 .Build();
1091
1092 ConverterSettings.OutputData = serializer.Serialize(ConverterSettings.InputClass);
1093
1094 if (true == appendToFile)
1095 File.AppendAllText(filename, ConverterSettings.OutputData);
1096 else
1097 File.WriteAllText(filename, ConverterSettings.OutputData);
1098 }
1099 else if (0 < ConverterSettings.InputDictionary.Count)
1100 {
1101 // Aliases let YAML round-trip shared references, but enabling them makes YamlDotNet
1102 // walk the whole graph an extra time to find repeat references. On a large flat
1103 // collection (e.g. tens of thousands of sitemap URLs) that pre-pass is what stalls
1104 // Serialize, and such data has no shared references worth anchoring. So keep aliases
1105 // for normal payloads and only drop them once the graph is large enough to matter.
1106 const int aliasGraphLimit = 5000;
1107 int yamlIdx = InputWithin(inputIdx, ConverterSettings.InputDictionary.Count, "input");
1108 bool largeGraph = ConverterHelper.CountGraphNodes(ConverterSettings.InputDictionary[yamlIdx], aliasGraphLimit) >= aliasGraphLimit;
1109
1110 var serializerBuilder = new SerializerBuilder()
1111 .WithEventEmitter(next => new ExplicitNullYamlEventEmitter(next))
1112 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
1113 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1114 .WithTypeConverter(new IPAddressYamlConverter())
1115 .WithTypeConverter(new ComplexYamlConverter())
1116 .WithTypeConverter(new UriYamlConverter())
1117 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1118 .WithTypeConverter(new NullableGuidYamlConverter())
1119 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1120 .WithTypeConverter(new VersionYamlConverter())
1121 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1122 .WithTypeConverter(new QuotedStringYamlConverter())
1123 .WithTypeConverter(new WaitTimeYamlConverter());
1124
1125 if (true == largeGraph)
1126 serializerBuilder = serializerBuilder.DisableAliases();
1127
1128 var serializer = serializerBuilder.Build();
1129
1130 object CleanForYaml(object input)
1131 {
1132 if (input is Dictionary<object, dynamic> d)
1133 {
1134 // First: detect if this dict came from XML (has @keys or #text)
1135 bool isXmlDerived = d.Keys.Cast<object>().Any(k =>
1136 k.ToString() == "#text" || k.ToString().StartsWith("@"));
1137
1138 if (ConverterHelper.IsArrayLike(d))
1139 {
1140 // Surrogate array - collect in order
1141 var list = new List<object>();
1142 foreach (var p in d)
1143 {
1144 string k = p.Key.ToString();
1145
1146 if (k.StartsWith("GPALKEY"))
1147 {
1148 list.Add(CleanForYaml(p.Value));
1149 }
1150 }
1151 return list;
1152 }
1153 else
1154 {
1155 // Normal object
1156 var cleanDict = new Dictionary<object, dynamic>();
1157
1158 string textValue = null;
1159 if (d.TryGetValue("#text", out var txt) && isXmlDerived)
1160 {
1161 textValue = txt?.ToString();
1162 }
1163
1164 // Collect attributes (remove @ prefix)
1165 foreach (var p in d)
1166 {
1167 string k = p.Key.ToString();
1168
1169 k = k.Replace(@"GPAL_", "");
1170
1171 if (k.StartsWith("@") && isXmlDerived)
1172 {
1173 cleanDict[k.Substring(1)] = p.Value;
1174 }
1175 }
1176
1177 // Collect child elements
1178 foreach (var p in d)
1179 {
1180 string k = p.Key.ToString();
1181
1182 k = k.Replace(@"GPAL_", "");
1183
1184 if (k == "#text" || k.StartsWith("@") || k.StartsWith("GPALKEY"))
1185 continue;
1186
1187 cleanDict[k] = CleanForYaml(p.Value);
1188 }
1189
1190 // Flatten leaf nodes: if only text + attributes -> add "value"
1191 if (isXmlDerived && textValue != null)
1192 {
1193 if (cleanDict.Count == 0)
1194 {
1195 // Pure text node -> flatten to scalar
1196 if (double.TryParse(textValue, out double num))
1197 return num;
1198 return textValue;
1199 }
1200 else
1201 {
1202 // Has attributes -> add value field
1203 cleanDict["value"] = textValue;
1204 }
1205 }
1206
1207 return cleanDict;
1208 }
1209 }
1210 else if (input is IEnumerable enumerable && !(input is string))
1211 {
1212 var list = new List<object>();
1213 foreach (var item in enumerable)
1214 {
1215 list.Add(CleanForYaml(item));
1216 }
1217 return list;
1218 }
1219
1220 return input;
1221 }
1222
1223 Dictionary<object, dynamic> rootValue = ConverterSettings.InputDictionary[yamlIdx];
1224 dynamic cleanRoot = CleanForYaml(rootValue);
1225 cleanRoot = ConverterHelper.StripGpalKeys(cleanRoot);
1226
1227 if (true == ConverterHelper.IsEnumerableObject(cleanRoot) && !(cleanRoot is IDictionary) && !(cleanRoot is string))
1228 {
1229 cleanRoot = new Dictionary<object, object>
1230 {
1231 ["values"] = cleanRoot
1232 };
1233 }
1234
1235 ConverterSettings.OutputData = serializer.Serialize(cleanRoot);
1236
1237 if (true == appendToFile)
1238 File.AppendAllText(filename, ConverterSettings.OutputData);
1239 else
1240 File.WriteAllText(filename, ConverterSettings.OutputData);
1241 }
1242 else
1243 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1244 break;
1245 case DataFormat.HTML:
1246 if (0 < ConverterSettings.InputDictionary.Count)
1247 {
1248 switch (ConverterSettings.InDataFormat[inputIdx])
1249 {
1250 case DataFormat.CARET:
1251 case DataFormat.COLON:
1252 case DataFormat.CSV:
1253 case DataFormat.CUSTOM_DELIMITER:
1254 case DataFormat.DOT:
1255 case DataFormat.HYPHEN:
1256 case DataFormat.LOG: // a finished line of text, written and read as it stands
1257 case DataFormat.PIPE:
1258 case DataFormat.SEMICOLON:
1259 case DataFormat.SPACE:
1260 case DataFormat.TAB:
1261 case DataFormat.PRN:
1262 {
1263 var sb = new StringBuilder();
1264
1265 var grid = ConverterSettings.InputGrid; // List<List<string>>
1266 if (grid == null || grid.Count == 0)
1267 {
1268 ConverterSettings.OutputData = "<p>No data</p>";
1269 break;
1270 }
1271
1272 int gridIdx = InputWithin(inputIdx, grid.Count, "input grid");
1273
1274 sb.AppendLine("<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; font-family: Arial, sans-serif; width: 100%;\">");
1275
1276 // Determine headers
1277 List<string> headers = null;
1278 int dataStartRow = 0;
1279
1280 if (ConverterSettings.ColumnNames?.Count() > fileIdx && ConverterSettings.ColumnNames[fileIdx].Count > 0)
1281 {
1282 headers = ConverterSettings.ColumnNames[fileIdx];
1283 dataStartRow = 0;
1284 }
1285 else if (ConverterSettings.FirstLineIsColumnHeaders && grid.Count > 0)
1286 {
1287 headers = grid[gridIdx][0];
1288 dataStartRow = 1;
1289 }
1290
1291 // Emit <thead> if we have headers
1292 if (headers != null)
1293 {
1294 sb.AppendLine(" <thead>");
1295 sb.AppendLine(" <tr>");
1296 foreach (var h in headers)
1297 {
1298 string safeHeader = System.Net.WebUtility.HtmlEncode(h ?? "");
1299 sb.AppendLine($" <th>{safeHeader}</th>");
1300 }
1301 sb.AppendLine(" </tr>");
1302 sb.AppendLine(" </thead>");
1303 }
1304
1305 // Emit <tbody>
1306 sb.AppendLine(" <tbody>");
1307
1308 for (int rowIdx = dataStartRow; rowIdx < grid[gridIdx].Count(); rowIdx++)
1309 {
1310 var row = grid[gridIdx][rowIdx];
1311 sb.AppendLine(" <tr>");
1312
1313 // Pad or truncate row to match column count if headers exist
1314 int colCount = headers?.Count ?? row.Count();
1315 for (int colIdx = 0; colIdx < colCount; colIdx++)
1316 {
1317 string cell = (colIdx < row.Count()) ? (row[colIdx].ToString() ?? "") : "";
1318 cell = cell.Trim(ConverterSettings.InDelimiter.Value);
1319 string safeCell = System.Net.WebUtility.HtmlEncode(cell);
1320 sb.AppendLine($" <td>{safeCell}</td>");
1321 }
1322
1323 sb.AppendLine(" </tr>");
1324 }
1325
1326 sb.AppendLine(" </tbody>");
1327 sb.AppendLine("</table>");
1328
1329 ConverterSettings.OutputData = sb.ToString();
1330 }
1331 break;
1332 default:
1333 foreach (Dictionary<object, dynamic> dict in ConverterSettings.InputDictionary)
1334 {
1335 // 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>
1336 // Fix loading the dictioanry with multiple copies
1337 if (true == ConverterSettings.OutputData?.Contains("</body>"))
1338 break;
1339
1340 try
1341 {
1342 if (dict["tag"].ToString() == "#text") // htmlnodes contain text as child nodes, but also a duplicate #text tag following every tag
1343 continue;
1344 }
1345 catch
1346 {
1347 // it's ok, move on...
1348 }
1349 stringBuilder.AppendLine(ConverterHelper.ConvertDictionaryToHtml(dict));
1350
1351 if (0 < stringBuilder.Length)
1352 {
1353 ConverterSettings.OutputData += stringBuilder.ToString();
1354 stringBuilder.Clear();
1355 }
1356 }
1357 break;
1358 }
1359
1360 if (true == appendToFile)
1361 File.AppendAllText(filename, ConverterSettings.OutputData);
1362 else
1363 File.WriteAllText(filename, ConverterSettings.OutputData);
1364 }
1365 else
1366 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No input data to write to [{filename}]", this, GPALObjectType.Converter);
1367 break;
1368 }
1369 fileIdx++;
1370 }
1371 }
1372 else if (null != ConverterSettings.OutputDatabase)
1373 {
1374 // a grid is rows of parameters and the write runs once per row, so a row holding two values is
1375 // two parameters. an input with no grid behind it, a plain string, has no rows at all and used to
1376 // index an empty list here, so it becomes one row holding that value: the single column case
1377 IGPALGrid<string> parameterGrid;
1378
1379 if (0 < ConverterSettings.InputGrid.Count && null != ConverterSettings.InputGrid[0])
1380 parameterGrid = ConverterSettings.InputGrid[0];
1381 else
1382 {
1383 parameterGrid = GPAL.Grid.ToGPALObject();
1384
1385 foreach (string inputValue in ConverterSettings.InputData)
1386 parameterGrid.AddRow(new List<string> { inputValue });
1387 }
1388
1389 ConverterSettings.OutputDatabase.Create.ClearParameters.WithParameterGrid(parameterGrid).Execute(out int rowCOunt);
1390 }
1391 else if (DataFormat.GRID == ConverterSettings.OutDataFormat && null != ConverterSettings.OutputGrid
1392 && (null != ConverterSettings.InputFile || true == ConverterSettings.InDataFormat.Contains(DataFormat.GRID)))
1393 {
1394 // rows in, rows out. the object output below reaches a caller's grid through a copy gated on its
1395 // own converted result having items, and for a grid that result is empty, so a file read into a
1396 // grid answered with nothing at all. these are the rows as they were read, and they have to be
1397 // taken here rather than after Convert returns because the settings are cleared on the way out
1398 foreach (IGPALGrid<string> inGrid in ConverterSettings.InputGrid)
1399 foreach (List<string> gridRow in inGrid)
1400 ConverterSettings.OutputGrid.AddRow(new List<string>(gridRow));
1401 }
1402 else if (null != ConverterSettings.OutputClassType)
1403 {
1404 bool yamlDirectDone = false;
1405 if (ConverterSettings.InDataFormat.Count > 0 && ConverterSettings.InDataFormat[0] == DataFormat.YAML
1406 && ConverterSettings.InputData.Count > 0)
1407 {
1408 // Struct types can't be pre-allocated during deserialization, so a YAML alias that
1409 // points back through a struct creates infinite recursion — StackOverflowException
1410 // which can't be caught. Pre-scan for aliases; if any exist fall through to COFD.
1411 bool hasAliases = false;
1412 var preParser = new Parser(new System.IO.StringReader(ConverterSettings.InputData[0]));
1413 while (preParser.MoveNext())
1414 {
1415 if (preParser.Current is AnchorAlias)
1416 {
1417 hasAliases = true;
1418 break;
1419 }
1420 }
1421
1422 if (!hasAliases)
1423 {
1424 try
1425 {
1426 var deserializer = new DeserializerBuilder()
1427 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1428 .WithTypeConverter(new IPAddressYamlConverter())
1429 .WithTypeConverter(new ComplexYamlConverter())
1430 .WithTypeConverter(new UriYamlConverter())
1431 .WithTypeConverter(new NullableBigIntegerYamlConverter())
1432 .WithTypeConverter(new NullableGuidYamlConverter())
1433 .WithTypeConverter(new NullableTimeSpanYamlConverter())
1434 .WithTypeConverter(new VersionYamlConverter())
1435 .WithTypeConverter(new DateTimeOffsetYamlConverter())
1436 .WithTypeConverter(new QuotedStringYamlConverter())
1437 .WithTypeConverter(new WaitTimeYamlConverter())
1438 .IgnoreUnmatchedProperties()
1439 .Build();
1440
1441 ConverterSettings.OutputClass = deserializer.Deserialize(ConverterSettings.InputData[0], ConverterSettings.OutputClassType);
1442 yamlDirectDone = true;
1443 }
1444 catch { }
1445 }
1446 }
1447
1448 if (!yamlDirectDone)
1449 {
1450 // XML input arrives with GPAL numeric key prefixes and all values wrapped in
1451 // List<Dict> + #text nodes. Strip/flatten the whole structure once before
1452 // any POCO reconstruction so CreateObjectFromDictionary sees clean keys and scalars.
1453 object classInput = ConverterSettings.InputDictionary;
1454 if (ConverterSettings.InDataFormat.Count > 0 && ConverterSettings.InDataFormat[0] == DataFormat.XML)
1455 classInput = ConverterHelper.StripGpalKeys(ConverterSettings.InputDictionary);
1456
1457 if (true == ConverterSettings.OutputClassType.IsGenericType && true == ConverterHelper.IsDictionaryType(ConverterSettings.OutputClassType) && 0 < ConverterSettings.InputDictionary.Count)
1458 {
1459 ConverterSettings.OutputClass = ConverterHelper.ConvertToClass(classInput, ConverterSettings.OutputClassType, classInput, ConverterSettings.ColumnNames?[0]); // NOTE: CAVEAT: hardcoded value
1460 }
1461 else if (true == ConverterSettings.OutputClassType.IsGenericType && null != ConverterSettings.OutputClassType.GetInterface("IEnumerable"))
1462 {
1463 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1464 if (ConverterSettings.OutputClassElementType == typeof(string))
1465 {
1466 var resultList = new List<string>();
1467 foreach (var dict in ConverterSettings.InputDictionary)
1468 {
1469 resultList.AddRange(BuildHierarchicalStrings(dict));
1470 }
1471 ConverterSettings.OutputClass = resultList;
1472 }
1473 else if (true == ConverterSettings.OutputClassElementType.IsArray)
1474 {
1475 try
1476 {
1477 // Ensure OutputClassElementType is valid
1478 Type elementType = ConverterSettings.OutputClassElementType;
1479 if (elementType == null && ConverterSettings.OutputClassType.IsGenericType)
1480 {
1481 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
1482 elementType = genericArgs.Length > 1 ? genericArgs[1] : genericArgs.FirstOrDefault() ?? typeof(object);
1483 }
1484 if (elementType == null)
1485 {
1486 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"OutputClassElementType is null for [{ConverterSettings.OutputClassType}]", null, GPALObjectType.Other);
1487 ConverterSettings.OutputClass = default(T);
1488 return this;
1489 }
1490
1491 var mi = typeof(ConverterHelper).GetMethod("CreateIEnumerableFromClass",
1492 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1493 null,
1494 new[] { typeof(ConverterSettings) },
1495 null);
1496
1497 var genericMethod = mi.MakeGenericMethod(new[] { elementType });
1498 ConverterSettings.OutputClass = (T)genericMethod.Invoke(null, new object[] { ConverterSettings });
1499 }
1500 catch (Exception ex)
1501 {
1502 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to create IEnumerable for [{ConverterSettings.OutputClassType}]: [{ex.Message}]", null, GPALObjectType.Other, ex);
1503 ConverterSettings.OutputClass = default(T);
1504 return this;
1505 }
1506 }
1507 else
1508 {
1509 try
1510 {
1511 // Ensure OutputClassElementType is valid
1512 Type elementType = ConverterSettings.OutputClassElementType;
1513 if (elementType == null && ConverterSettings.OutputClassType.IsGenericType)
1514 {
1515 var genericArgs = ConverterSettings.OutputClassType.GetGenericArguments();
1516 elementType = genericArgs.Length > 1 ? genericArgs[1] : genericArgs.FirstOrDefault() ?? typeof(object);
1517 }
1518 if (elementType == null)
1519 {
1520 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"OutputClassElementType is null for [{ConverterSettings.OutputClassType}]", null, GPALObjectType.Other);
1521 ConverterSettings.OutputClass = default(T);
1522 return this;
1523 }
1524
1525 // Get the MethodInfo for ConvertToClass
1526 var mi = typeof(ConverterHelper).GetMethod(
1527 "ConvertToClass",
1528 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1529 null,
1530 new[] { typeof(object), typeof(Type), typeof(object), typeof(List<string>) }, // Parameters dynamic dictList, Type targetType, dynamic parentDictionary, List<string> columnNames
1531 null);
1532
1533 // Invoke the method
1534 ConverterSettings.OutputClass = mi.Invoke(null, new object[] { ConverterSettings.InputDictionary, ConverterSettings.OutputClassType, ConverterSettings.InputDictionary, ConverterSettings.ColumnNames?[0] }); // NOTE: CAVEAT hardcoded value
1535 }
1536 catch (Exception ex)
1537 {
1538 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to create IEnumerable for [{ConverterSettings.OutputClassType}]: [{ex.Message}]", null, GPALObjectType.Other, ex);
1539 ConverterSettings.OutputClass = default(T);
1540 return this;
1541 }
1542 }
1543 MethodInfo addMethod = ConverterSettings.OutputClassType.GetMethod("Add",
1544 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1545 MethodInfo addRowMethod = ConverterSettings.OutputClassType.GetMethod("AddRow",
1546 BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(List<>).MakeGenericType(ConverterSettings.OutputClassElementType) }, null);
1547 MethodInfo enqueueMethod = ConverterSettings.OutputClassType.GetMethod("Enqueue",
1548 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1549 MethodInfo pushMethod = ConverterSettings.OutputClassType.GetMethod("Push",
1550 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
1551
1552 // It's an IEnumerable<OutputClass>
1553 int count = 0;
1554 try
1555 {
1556 count = ConverterSettings.OutputClass.Count;
1557 }
1558 catch
1559 {
1560 foreach (dynamic item in ConverterSettings.OutputClass)
1561 count++;
1562 }
1563
1564 try
1565 {
1566 if (0 < count)
1567 {
1568 if (null != addRowMethod) // GPAGrid
1569 {
1570 if (null == ConverterSettings.InputData && null != ConverterSettings.InputFile)
1571 {
1572 string directory = Path.GetDirectoryName(ConverterSettings.InputFile.Filenames[0]);
1573 string filenamepart = Path.GetFileName(ConverterSettings.InputFile.Filenames[0]);
1574
1575 string filename2 = Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart).FirstOrDefault();
1576 if (false == string.IsNullOrEmpty(filename2))
1577 {
1578 try
1579 {
1580 ConverterSettings.InputData.Add(File.ReadAllText(filename2));
1581 }
1582 catch (Exception ex)
1583 {
1584 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to read entire file [{filename2}] into memory. Output may be empty depending on output type.", this, GPALObjectType.Converter, ex);
1585 }
1586 }
1587 }
1588
1589 // COPY ConverterSettings.OutputClass to the ref outputClass.
1590 // once, not once per input: the inputs were read and merged at the top of
1591 // Convert, so by here there is one result for the lot. looping InputData again
1592 // copied that whole result over for every input there had been.
1593 foreach (var item in ConverterSettings.OutputClass)
1594 {
1595 if (null != addRowMethod) // GPALGrid
1596 {
1597 addRowMethod.Invoke(outputClass, new object[] { new List<string>() { (dynamic)item } });
1598 /* BUG - CAVEAT: duplicating data
1599 ConverterSettings.OutputData = inData;
1600 if (null != ConverterSettings.InDelimiter && null != ConverterSettings.OutDelimiter) // CAVEAT: BUG: NOTE: Should this be a list of delim?
1601 ConverterSettings.OutputData = inData.Replace(ConverterSettings.InDelimiter.Value, ConverterSettings.OutDelimiter.Value);
1602 string[] lines = ConverterSettings.OutputData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
1603
1604 // Enumerate over the lines using foreach loop
1605 foreach (object line in lines)
1606 addRowMethod.Invoke(outputClass, new object[] { new List<object>() { (dynamic)line } });
1607 */
1608 }
1609 }
1610 }
1611 else if (null != addMethod || null != enqueueMethod || null != pushMethod)
1612 {
1613 Type elementType = outputClass.GetType().IsGenericType ? outputClass.GetType().GetGenericArguments()[0] : null;
1614 if (elementType != null)
1615 {
1616 foreach (var compatibleItem in ConvertToCompatibleItems(ConverterSettings.OutputClass, elementType))
1617 {
1618 if (null != addMethod)
1619 addMethod.Invoke(outputClass, new object[] { compatibleItem });
1620 else if (null != enqueueMethod)
1621 enqueueMethod.Invoke(outputClass, new object[] { compatibleItem });
1622 else if (null != pushMethod)
1623 pushMethod.Invoke(outputClass, new object[] { compatibleItem });
1624 }
1625 }
1626 }
1627 else
1628 // we've already converted to this type, might that be the case for a gpal grid, too?
1629 outputClass = (T)(object)ConverterSettings.OutputClass;
1630
1631 return this;
1632 }
1633 }
1634 catch (Exception ex)
1635 {
1636 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Can't find 'Add/Enqueue/Push' method for [{ConverterSettings.OutputClassType}]", ConverterSettings, GPALObjectType.Other, ex);
1637 }
1638
1639 // not correct but what we are doing
1640 // ConverterSettings.OutputClass = ConverterHelper.CreateListFromClass<type>(ConverterSettings.InputDictionary);
1641 }
1642 else if (ConverterSettings.OutputClassType.IsGenericType && ConverterSettings.OutputClassType.GetGenericTypeDefinition() == typeof(List<>))
1643 {
1644 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1645 if (ConverterSettings.OutputClassElementType == typeof(string))
1646 {
1647 var resultList = new List<string>();
1648 foreach (var dict in ConverterSettings.InputDictionary)
1649 {
1650 // Build hierarchical strings with full paths
1651 resultList.AddRange(BuildHierarchicalStrings(dict));
1652 }
1653 ConverterSettings.OutputClass = resultList;
1654 ConverterSettings.OutputClass = (T)(object)resultList;
1655 }
1656 else
1657 {
1658 var mi = typeof(ConverterHelper).GetMethod("CreateListFromClass",
1659 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1660 null,
1661 new[] { typeof(ConverterSettings) },
1662 null);
1663 var fooRef = mi.MakeGenericMethod(new[] { ConverterSettings.OutputClassElementType });
1664 ConverterSettings.OutputClass = fooRef.Invoke(new ConverterHelper(), new object[] { ConverterSettings });
1665 }
1666 }
1667 else if (ConverterSettings.OutputClassType.IsArray && ConverterSettings.OutputClassType.GetElementType() == ConverterSettings.OutputClassElementType)
1668 {
1669 // It's an array of OutputClass[]
1670 // since we can have a dynamic type. we can't easily defined <T> inline, so we have to generate the method signature dynamically
1671 if (ConverterSettings.OutputClassElementType == typeof(string))
1672 {
1673 var resultList = new List<string>();
1674 foreach (var dict in ConverterSettings.InputDictionary)
1675 {
1676 foreach (var kvp in dict)
1677 {
1678 resultList.Add(kvp.Value?.ToString() ?? string.Empty);
1679 }
1680 }
1681 ConverterSettings.OutputClass = resultList;
1682 }
1683 else
1684 {
1685 var mi = typeof(ConverterHelper).GetMethod("CreateIEnumerableFromClass",
1686 BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
1687 null,
1688 new[] { typeof(ConverterSettings) },
1689 null);
1690
1691 var fooRef = mi.MakeGenericMethod(new[] { ConverterSettings.OutputClassElementType });
1692 ConverterSettings.OutputClass = fooRef.Invoke(new ConverterHelper(), new dynamic[] { ConverterSettings });
1693 }
1694 }
1695 //else if (ConverterSettings.OutputClassType == typeof(string))
1696 //{
1697 // outputClass = (T)(object)ConverterSettings.InputData.ToString();
1698 //}
1699 else if (ConverterSettings.OutputClassType == ConverterSettings.OutputClassType) // to prevent last else from firing, just yet... how to check this tho...
1700 {
1701 // It's a single OutputClass
1702 if (0 < ConverterSettings.InputDictionary.Count)
1703 ConverterSettings.OutputClass = ConverterHelper.ConvertToClass(classInput, ConverterSettings.OutputClassType, classInput, ConverterSettings.ColumnNames?[0]); // NOTE: CAVEAT: hardcoded value
1704 // ConverterHelper.LoadClassFromDictionary(ConverterSettings.OutputClass, ConverterSettings.InputDictionary[0]);
1705 }
1706 else
1707 {
1708 // Unknown type
1709 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown type [{ConverterSettings.OutputClass}].", this, GPALObjectType.Converter);
1710 }
1711 } // end else (non-YAML path)
1712
1713 try
1714 {
1715 if (outputClass != ConverterSettings.OutputClass)
1716 outputClass = (T)ConverterSettings.OutputClass;
1717 } catch (Exception)
1718 {
1719 // outputClass may already be populated? so then return it without setting it
1720 }
1721 }
1722 else if (ConverterSettings.OutDataFormat == DataFormat.STRING)
1723 {
1724 outputClass = (T)(object)ConverterSettings.InputData;
1725 }
1726 #endregion <Output>
1727
1728 ResetIdleTimer();
1729
1730 if (null != ConverterSettings.InputFile)
1731 {
1732 ((IGPALFileInternal)ConverterSettings.InputFile).FileDictionary.Clear();
1733 ((IGPALFileInternal)ConverterSettings.InputFile).FileData.Clear();
1734 ((IGPALFileInternal)ConverterSettings.InputFile).FileGrid.Clear();
1735 ((IGPALFileInternal)ConverterSettings.InputFile).AlreadyTokenized = false;
1736 }
1737
1738 return this;
1739 }
1740
1748 {
1749 ConverterSettings.RegisteredStructs.Add(knownStructType);
1750 return this;
1751 }
1752
1760 {
1761 throw new NotImplementedException();
1762 }
1763
1771 {
1772 throw new NotImplementedException();
1773 }
1774
1782 {
1783 throw new NotImplementedException();
1784 }
1785
1792 public IAllowConverterInputAndActions RenderToClass(out dynamic outputClass) // NOTE: would this be a thing different from savetoclass?
1793 {
1794 throw new NotImplementedException();
1795 }
1796 #region Helpers
1803 internal static DataFormat GetDataFormat(char delimiter)
1804 {
1805 switch (delimiter)
1806 {
1807 case ('^'):
1808 return DataFormat.CARET;
1809 case (':'):
1810 return DataFormat.COLON;
1811 case (','):
1812 return DataFormat.CSV;
1813 case ('.'):
1814 return DataFormat.DOT;
1815 case ('-'):
1816 return DataFormat.HYPHEN;
1817 case ('|'):
1818 return DataFormat.PIPE;
1819 case (' '):
1820 return DataFormat.SPACE;
1821 case (';'):
1822 return DataFormat.SEMICOLON;
1823 case ('\t'):
1824 return DataFormat.TAB;
1825 default:
1826 return DataFormat.CUSTOM_DELIMITER;
1827 }
1828 }
1829
1837 private List<string> BuildHierarchicalStrings(dynamic dictOrList, string prefix = "")
1838 {
1839 var result = new List<string>();
1840 if (dictOrList == null) return result;
1841
1842 // Handle dictionaries (like reporting, uses)
1843 if (ConverterHelper.IsDictionaryType(dictOrList.GetType()))
1844 {
1845 foreach (var kvp in dictOrList)
1846 {
1847 string key = kvp.Key.ToString();
1848 dynamic value = kvp.Value;
1849 string newPrefix = string.IsNullOrEmpty(prefix) ? "" : $"{prefix}.{key}";
1850
1851 if (ConverterHelper.IsDictionaryType(value.GetType()))
1852 {
1853 result.AddRange(BuildHierarchicalStrings(value));
1854 }
1855 else if (ConverterHelper.IsEnumerableType(value.GetType()) && value.GetType() != typeof(string))
1856 {
1857 int index = 0;
1858 foreach (var item in value)
1859 {
1860 string indexedPrefix = $"{newPrefix}[{index}]";
1861 result.AddRange(BuildHierarchicalStrings(item));
1862 index++;
1863 }
1864 }
1865 else
1866 {
1867 // Leaf node: add the key-value pair with the full path
1868 if (false == string.IsNullOrEmpty(newPrefix))
1869 result.Add($"{newPrefix}={value?.ToString() ?? ""}");
1870 else
1871 result.Add($"{value?.ToString() ?? ""}");
1872 }
1873 }
1874 }
1875 // Handle lists (like stored_procs)
1876 else if (ConverterHelper.IsEnumerableType(dictOrList.GetType()) && dictOrList.GetType() != typeof(string))
1877 {
1878 int index = 0;
1879 foreach (var item in dictOrList)
1880 {
1881 string indexedPrefix = $"{prefix}[{index}]";
1882 result.AddRange(BuildHierarchicalStrings(item, indexedPrefix));
1883 index++;
1884 }
1885 }
1886 else
1887 {
1888 // Leaf node: add the key-value pair with the full path
1889 result.Add($"{prefix}={dictOrList?.ToString() ?? ""}");
1890 }
1891
1892 return result;
1893 }
1901 private static IEnumerable<object> ConvertToCompatibleItems(object sourceItem, Type targetType)
1902 {
1903 var results = new List<object>();
1904
1905 if (sourceItem == null)
1906 return results;
1907
1908 // Direct compatibility check
1909 if (targetType.IsAssignableFrom(sourceItem.GetType()))
1910 {
1911 results.Add(sourceItem);
1912 return results;
1913 }
1914
1915 // Handle collections
1916 if (sourceItem is IEnumerable enumerable && !(sourceItem is string))
1917 {
1918 foreach (var nestedItem in enumerable)
1919 {
1920 results.AddRange(ConvertToCompatibleItems(nestedItem, targetType));
1921 }
1922 return results;
1923 }
1924
1925 // Handle dictionaries
1926 if (sourceItem is IDictionary dict)
1927 {
1928 foreach (var value in dict.Values)
1929 {
1930 if (value != null && targetType.IsAssignableFrom(value.GetType()))
1931 {
1932 results.Add(value);
1933 }
1934 else if (value != null)
1935 {
1936 results.AddRange(ConvertToCompatibleItems(value, targetType));
1937 }
1938 }
1939 return results;
1940 }
1941
1942 // Try type conversion
1943 var convertedItems = new List<object>();
1944 var converter = TypeDescriptor.GetConverter(targetType);
1945 if (converter != null && converter.CanConvertFrom(sourceItem.GetType()))
1946 {
1947 try
1948 {
1949 var converted = converter.ConvertFrom(sourceItem);
1950 convertedItems.Add(converted);
1951 }
1952 catch
1953 {
1954 // Skip items that can't be converted
1955 }
1956 }
1957 else if (targetType == typeof(string))
1958 {
1959 try
1960 {
1961 convertedItems.Add(sourceItem.ToString());
1962 }
1963 catch
1964 {
1965 // Skip items that can't be converted
1966 }
1967 }
1968
1969 results.AddRange(convertedItems);
1970 return results;
1971 }
1972
1973 internal static class DataFormatDetector
1974 {
1975 // Helper to detect the best DataFormat from input string.
1976 // Focuses on XML, JSON, YAML, CSV first, then others via heuristics.
1977 // Never throws - returns NOTSET on failure, publishes GPAL warnings.
1978 // Assumptions: Trims input, checks startsWith/patterns, tries light parsing.
1979 // For delimiters (CSV, TAB, etc.): Checks first few lines for consistent column count.
1980 // Add NuGets if needed: Newtonsoft.Json (JSON), YamlDotNet (YAML full parse - optional here).
1988 public static DataFormat DetectFormat(string input)
1989 {
1990 if (string.IsNullOrWhiteSpace(input))
1991 {
1992 GPAL.PublishSimpleEvent(
1993 GPALEventType.WARNING,
1994 "DetectFormat called with empty/null input. Returning NOTSET.",
1995 typeof(DataFormatDetector),
1996 GPALObjectType.Other);
1997
1998 return DataFormat.NOTSET;
1999 }
2000
2001 string trimmed = input.Trim();
2002
2003 // Early check: is this likely base64-encoded data?
2004 //if (IsLikelyBase64(trimmed))
2005 //{
2006 // return DataFormat.BASE64;
2007 //}
2008
2009 string firstLine = trimmed.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
2010
2011 // XML: Starts with <, valid tags (light check)
2012 if (trimmed.StartsWith("<") && IsLikelyXml(trimmed))
2013 {
2014 return DataFormat.XML;
2015 }
2016
2017 // JSON: Starts with { or [, try parse
2018 if ((trimmed.StartsWith("{") || trimmed.StartsWith("[")) && IsValidJson(trimmed))
2019 {
2020 return DataFormat.JSON;
2021 }
2022
2023 // YAML: Key: value patterns, no {/[ start, try basic check (full parse if YamlDotNet added)
2024 if (!trimmed.StartsWith("{") && !trimmed.StartsWith("[") && !trimmed.StartsWith("<") && IsLikelyYaml(trimmed))
2025 {
2026 return DataFormat.YAML;
2027 }
2028
2029 // HTML: Starts with <html> or <!DOCTYPE html> or has <body> etc.
2030 if (trimmed.StartsWith("<!DOCTYPE html>") || trimmed.StartsWith("<html") || Regex.IsMatch(trimmed, @"<body|</body|<head|</head", RegexOptions.IgnoreCase))
2031 {
2032 return DataFormat.HTML;
2033 }
2034
2035 // PDF: Starts with %PDF- (binary-ish, but string might be base64 or partial)
2036 if (trimmed.StartsWith("%PDF-"))
2037 {
2038 return DataFormat.PDF;
2039 }
2040
2041 // Tabular/Delimited: CSV, TAB, PIPE, SEMICOLON, SPACE/PRN, CARET, HYPHEN, COLON, DOT, CUSTOM_DELIMITER
2042 DataFormat delimFormat = DetectDelimiterFormat(trimmed);
2043 if (delimFormat != DataFormat.NOTSET)
2044 {
2045 return delimFormat;
2046 }
2047
2048 // Others: DATABASE (SQL dump?), GRID (maybe table-like), DICTIONARY (key=value?), CLASS (C# code?), STRING (fallback)
2049 // For now, fallback to STRING if none match
2050 if (Regex.IsMatch(firstLine, @"^\s*CREATE TABLE|SELECT|INSERT|UPDATE", RegexOptions.IgnoreCase))
2051 {
2052 return DataFormat.DATABASE;
2053 }
2054 else if (Regex.IsMatch(trimmed, @"class\s+\w+\s*{|public\s+class", RegexOptions.IgnoreCase))
2055 {
2056 return DataFormat.CLASS;
2057 }
2058 else if (IsKeyValueLike(trimmed, "="))
2059 {
2060 return DataFormat.DICTIONARY;
2061 }
2062
2063 GPAL.PublishSimpleEvent(
2064 GPALEventType.DEBUG,
2065 "No clear DataFormat detected for input starting with [" + firstLine.Substring(0, Math.Min(50, firstLine.Length)) + "]. Returning STRING.",
2066 input,
2067 GPALObjectType.Other);
2068
2069 return DataFormat.STRING;
2070 }
2071
2072 // Returns true if the string strongly resembles base64 encoding
2079 private static bool IsLikelyBase64(string s)
2080 {
2081 if (string.IsNullOrEmpty(s)) return false;
2082 if (s.Length < 12) return false; // too short to be useful base64
2083
2084 string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
2085
2086 // must contain only base64 characters
2087 foreach (char c in s)
2088 {
2089 if (alphabet.IndexOf(c) == -1)
2090 {
2091 return false;
2092 }
2093 }
2094
2095 // length should be multiple of 4 (or close, allowing for missing padding)
2096 int mod = s.Length % 4;
2097 if (mod != 0)
2098 {
2099 // accept common unpadded or incorrectly padded cases
2100 if (mod != 2 && mod != 0)
2101 {
2102 return false;
2103 }
2104 }
2105
2106 // rough entropy check: base64 is never all same character or very low variety
2107 // (this is optional but helps reject garbage)
2108 int unique = new HashSet<char>(s).Count;
2109 if (unique < 5 && s.Length > 20)
2110 {
2111 return false;
2112 }
2113
2114 return true;
2115 }
2121 private static bool IsLikelyXml(string input)
2122 {
2123 try
2124 {
2125 using (var reader = XmlReader.Create(new StringReader(input)))
2126 {
2127 while (reader.Read()) { } // Light parse
2128 }
2129 return true;
2130 }
2131 catch
2132 {
2133 return false;
2134 }
2135 }
2136
2142 private static bool IsValidJson(string input)
2143 {
2144 try
2145 {
2146 JsonConvert.DeserializeObject(input); // Or use System.Text.Json.JsonDocument.Parse(input)
2147 return true;
2148 }
2149 catch
2150 {
2151 return false;
2152 }
2153 }
2154
2161 private static bool IsLikelyYaml(string input)
2162 {
2163 // Basic heuristic: lines with key: value, indents, no XML/JSON starts
2164 // If YamlDotNet added: try YamlStream.Load(new StringReader(input))
2165 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
2166 int yamlPatterns = lines.Count(line => Regex.IsMatch(line.Trim(), @"^[\w\-]+:\s*(.+|$)") || line.StartsWith(" ") || line.StartsWith("- "));
2167 return yamlPatterns > 2 && lines.Length > 1; // Threshold
2168 }
2169
2177 private static DataFormat DetectDelimiterFormat(string input)
2178 {
2179 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
2180 .Take(5) // Check header + few rows
2181 .ToArray();
2182
2183 if (lines.Length < 2) return DataFormat.NOTSET; // Need at least header + data
2184
2185 // Common delimiters
2186 var delimiters = new Dictionary<char, DataFormat>
2187 {
2188 { ',', DataFormat.CSV },
2189 { '\t', DataFormat.TAB },
2190 { '|', DataFormat.PIPE },
2191 { ';', DataFormat.SEMICOLON },
2192 { ' ', DataFormat.SPACE }, // PRN is space-delimited
2193 { '^', DataFormat.CARET },
2194 { '-', DataFormat.HYPHEN },
2195 { ':', DataFormat.COLON },
2196 { '.', DataFormat.DOT }
2197 };
2198
2199 foreach (var kvp in delimiters)
2200 {
2201 if (IsConsistentDelimiter(lines, kvp.Key))
2202 {
2203 if (kvp.Key == ' ') return DataFormat.PRN; // Special for space
2204 return kvp.Value;
2205 }
2206 }
2207
2208 // Custom if mixed but consistent columns
2209 if (lines.Select(line => line.Split(new[] { ',', ';', '\t', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries).Length).Distinct().Count() == 1)
2210 {
2211 return DataFormat.CUSTOM_DELIMITER;
2212 }
2213
2214 // Grid if looks like fixed-width columns
2215 if (lines.All(line => line.Length == lines[0].Length) && Regex.IsMatch(lines[0], @"[a-zA-Z0-9]+\s+[a-zA-Z0-9]+"))
2216 {
2217 return DataFormat.GRID;
2218 }
2219
2220 return DataFormat.NOTSET;
2221 }
2222
2230 private static bool IsConsistentDelimiter(string[] lines, char delim)
2231 {
2232 var colCounts = lines.Select(line => line.Count(c => c == delim) + 1) // Columns = delim count + 1
2233 .ToArray();
2234
2235 // Consistent if all rows have same col count, and >1 cols, and not too few delims
2236 return colCounts.Distinct().Count() == 1 && colCounts[0] > 1 && colCounts[0] == colCounts.Max();
2237 }
2238
2246 private static bool IsKeyValueLike(string input, string separator)
2247 {
2248 var lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
2249 return lines.Count(line => line.Contains(separator) && line.Split(separator.ToCharArray()).Length == 2) > 2;
2250 }
2251 }
2252
2260 private string FormatPretty(string input)
2261 {
2262 if (string.IsNullOrWhiteSpace(input))
2263 return input;
2264
2265 var format = ConverterSettings.InDataFormat.FirstOrDefault();
2266
2267 try
2268 {
2269 switch (format)
2270 {
2271 case DataFormat.JSON:
2272 var jsonOptions = new System.Text.Json.JsonSerializerOptions
2273 {
2274 WriteIndented = true,
2275 Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
2276 };
2277 var jsonElement = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(input);
2278 return System.Text.Json.JsonSerializer.Serialize(jsonElement, jsonOptions);
2279
2280 case DataFormat.XML:
2281 var xmlDoc = System.Xml.Linq.XDocument.Parse(input, System.Xml.Linq.LoadOptions.PreserveWhitespace);
2282 return xmlDoc.ToString(System.Xml.Linq.SaveOptions.None);
2283
2284 case DataFormat.HTML:
2285 // Prefer HtmlAgilityPack for real HTML, fallback to XDocument
2286 try
2287 {
2288 var htmlDoc = new HtmlAgilityPack.HtmlDocument();
2289 htmlDoc.LoadHtml(input);
2290 return htmlDoc.DocumentNode.WriteContentTo(); // or .OuterHtml with formatting
2291 }
2292 catch
2293 {
2294 var xDoc = System.Xml.Linq.XDocument.Parse(input, System.Xml.Linq.LoadOptions.PreserveWhitespace);
2295 return xDoc.ToString(System.Xml.Linq.SaveOptions.None);
2296 }
2297
2298 case DataFormat.YAML:
2299 var deserializer = new DeserializerBuilder().Build();
2300 var serializer = new SerializerBuilder()
2301 .WithIndentedSequences()
2302 .WithEventEmitter(next => new HumanReadableAliasEventEmitter(next))
2303 .Build();
2304 var obj = deserializer.Deserialize<object>(input);
2305 return serializer.Serialize(obj);
2306
2307 // These formats cannot/should not be pretty-printed
2308 case DataFormat.CSV:
2309 case DataFormat.TAB:
2310 case DataFormat.LOG: // a finished line of text, written and read as it stands
2311 case DataFormat.PIPE:
2312 case DataFormat.COLON:
2313 case DataFormat.SEMICOLON:
2314 case DataFormat.SPACE:
2315 case DataFormat.HYPHEN:
2316 case DataFormat.CARET:
2317 case DataFormat.PRN:
2318 case DataFormat.BASE64:
2319 case DataFormat.DATABASE:
2320 case DataFormat.XLSX:
2321 case DataFormat.PDF:
2322 case DataFormat.STRING:
2323 case DataFormat.CLASS:
2324 case DataFormat.DICTIONARY:
2325 case DataFormat.DOT:
2326 case DataFormat.GRID:
2327 return input;
2328
2329 default:
2330 return input;
2331 }
2332 }
2333 catch (Exception ex)
2334 {
2335 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"PrettyPrint failed for [{format}] [{ex.Message}]");
2336 return input; // safe fallback
2337 }
2338 }
2339 #endregion Helpers
2340 }
2341}
2342
2343
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....
IAllowConverterInputAndActions RenderTo(GPALFile outputFile)
Renders the configured input as a visual document (e.g. PDF or image) rather than a text data format,...
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....
IAllowConverterInputAndActions RenderToClass(out dynamic outputClass)
Renders the configured input as a visual document (e.g. PDF or image) rather than a text data format,...
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 SaveTo(string outputData)
Converts the configured input to a string in the configured (or detected) output format.
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.
IAllowConverterInputAndActions RenderTo(IGPALDatabase outputDatabase)
Renders the configured input as a visual document (e.g. PDF or image) rather than a text data format,...
IAllowConverterSettingsAndActions WithInput(IGPALGrid< string > inputGrid)
Sets the input to a GPAL grid of strings (GRID format, comma input delimiter), clearing any previousl...
IAllowConverterInputAndActions RenderTo(string outputData)
Renders the configured input as a visual document (e.g. PDF or image) rather than a text data format,...
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:546
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:474
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:2406