/
xml-csharp-cereal.js
1743 lines (1694 loc) · 76.3 KB
/
xml-csharp-cereal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'use strict';
/**
* Node.js XML serializer with an eye toward limited C# XmlSerializer compatibility
* @module xml-csharp-cereal
* @license (Unlicense OR Apache-2.0) DISCLAIMER: Authors and contributors assume no liability or warranty. Use at your own risk.
*/
// self executing anon func can wrap code for CommonJS or classic script, but not for ES6 module as export must be top level
//? if (typeof CLASSIC !== 'undefined')
//?= '(function() {'
const MyExports = {}; // lets collect all the exports in one object to make it easier to handle different module systems (see bottom for actual export)
// We need a DOMImplementation in order to create a xml doc via xmldom method (unless we force user to pass one to us?)
//const xmldom = TryRequire('xmldom');
var myDOMImplementation = null;
function getDOMImplementation()
{
if (myDOMImplementation) return myDOMImplementation; // if we already made one, use it
// see if xmldom is available, first via 'document.implementation', then by requiring xmldom package
if (typeof(document) !== 'undefined' && typeof(document.implementation) !== 'undefined') myDOMImplementation = document.implementation;
else
{
var xmldom = TryRequire('xmldom');
if (xmldom==null) return null;
myDOMImplementation = new xmldom.DOMImplementation();
}
return myDOMImplementation;
}
function TryRequire(name)
{
// check err.code === 'MODULE_NOT_FOUND' ? or require.resolve() ?
try { return require(name); }
catch (e) { return null; }
}
/**
* Extend standard Error object with additional information from XML serialization process
*/
class XmlSerializerError extends Error
{
/**
* Creates an instance of XmlSerializerError
* @param {Error|string} msg Error object or error message string
* @param {?Object} [opts=null] Options object that was used at time of the error
* @param {?Object} [_state=null] Internal state object at time of the error
*/
constructor(msg, opts = null, _state = null)
{
if (_state==null) _state={};
// In order to get object path into stack output, we need to add it to the message.
// Error.stack is a getter, so we can change the message even though the stack has been established.
// (One could override the getter, if one wanted to go that far https://stackoverflow.com/a/35392881 )
var path_str = '{' + XmlSerializerError.pathArrToStr(_state.ObjPath) + '} ';
_state.LastError = msg; // our state is in error now
if (msg instanceof Error)
{
msg.message = path_str + msg.message;
super(msg.message); // use no args or just give it message?
//this.stack = msg.stack;
Object.assign(this, msg); // assign all props from original error
}
else
{
super(path_str + msg); // technically super would create an initial stack at this exact line?
if (typeof(Error.captureStackTrace)=='function') { Error.captureStackTrace(this, this.constructor); } // this generates a new stack omitting this.constructor
}
this.name = this.constructor.name;
this.options = opts;
this.state = _state;
}
toString()
{
//if (this.objectPath) return this.name + ' @ ' + this.objectPath + ': ' + this.message;
return this.name + ': ' + this.message;
}
static pathArrToStr(arr)
{
if (arr==null) return '?';
// TODO - should manually build this string and put numbers in square brackets?
// TODO - instead of array of strings, would array of XmlTemplateItems be better? for XmlPassthrough just have string or null prop name?
return arr.join('/');
}
}
// region "Some parsers for primitive node values" -----
/**
* This callback takes value from XML and decodes it into appropriate value for object.
* @callback DecoderCallback
* @param {any} val the XML node string to decode
* @return {any} the decoded JS property value
*/
MyExports.decodeInt = function decodeInt(val)
{
var n = parseInt(val);
if (Number.isFinite(n)) return n;
throw new Error('Value cannot be parsed to int (' + val + ')');
}
MyExports.decodeFloat = function decodeFloat(val)
{
var n = parseFloat(val);
if (Number.isFinite(n)) return n;
throw new Error('Value cannot be parsed to float (' + val + ')');
}
MyExports.decodeDouble = function decodeDouble(val)
{
// parseFloat is effectively parseDouble
var n = parseFloat(val);
if (Number.isFinite(n)) return n;
throw new Error('Value cannot be parsed to float (' + val + ')');
}
MyExports.decodeBool = function decodeBool(val)
{
// if object, try ValueOf or toString? is that presuming too much?
if (MyExports.IsString(val))
{
if (val.toUpperCase()=='TRUE') return true;
if (val.toUpperCase()=='FALSE') return false;
try
{
var num = MyExports.decodeInt(val); // if '1' or '0' perhaps?
return (num != 0); // any non-zero value is true?
}
catch (e) { throw new Error('decodeBool cannot parse "' + val + '"'); }
}
else
{
return (val ? true : false); // rely on falsy/truthy ?
}
}
MyExports.decodeString = function decodeString(val)
{
// process undefined the same as null or treat it as an error?
if (val==null) return null; // should we return null or "" ? null seems more in tune with C# behavior?
return val.toString(); // just in case it isn't already a string?
}
MyExports.decodeDateTime = function decodeDateTime(val)
{
// ISO 8601 i.e. '2018-05-30T17:30:00'
// process undefined the same as null or treat it as an error?
if (val==null) return null; // could be nullable
return new Date(val);
}
//Might want to defer due to the many possible approaches and external libraries? Maybe provide a basic 'as seconds' default?
MyExports.decodeTimeSpan = function decodeTimeSpan(val)
{
// ISO 8601 i.e. 'P1DT10H17M36.789S'
// There's moment.js, TimeSpan.js, etc libraries to choose from for fuller feature sets?
// process undefined the same as null or treat it as an error?
if (val==null) return null; // could be nullable
// adapted from http://momentjs.com [MIT] (citing http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html)
// see isoRegex in 'moment/src/lib/duration/create.js'
var isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;
var match = isoRegex.exec(val);
if (match)
{
// Based on some info from https://stackoverflow.com/a/34532410 and https://stackoverflow.com/a/12466271
var secs = TS_ValOrZero(match[2])*31556926 // years
+TS_ValOrZero(match[3])*2629743.83 // months
+TS_ValOrZero(match[4])*604800 // weeks
+TS_ValOrZero(match[5])*86400 // days
+TS_ValOrZero(match[6])*3600 // hours
+TS_ValOrZero(match[7])*60 // mintues
+TS_ValOrZero(match[8]); // seconds
return (match[1] === '-' ? -secs : secs);
}
throw new Error('Cannot parse ISO time span "' + val + '"');
}
function TS_ValOrZero(val)
{
// adapted from http://momentjs.com [MIT] function parseIso() to use in decodeTimeSpan()
// we don't need to deal with sign here, cause we apply that after we get total seconds
// val is regex result which is either a string or undefined
if (val==undefined) return 0;
val = parseFloat(val.replace(',', '.')); // I assume the replace is for locales that use comma for decimals?
return (Number.isFinite(val) ? val : 0);
}
/**
* This callback takes value from object and encodes it into appropriate value for XML.
* @callback EncoderCallback
* @param {any} val the JS property value to encode
* @return {string} the encoded XML node string
*/
MyExports.encodeString = function encodeString(val)
{
return val.toString(); // just in case it isn't already a string?
}
MyExports.encodeBool = function encodeBool(val)
{
return (val ? 'true' : 'false'); // c sharp xml prints the lowercase string
}
MyExports.encodePassthrough = function encodePassthrough(val)
{
// numbers should passthrough fine, unless XML library needs them an explicit type?
if (val instanceof Number) return val.valueOf(); // unwrap if in Number object ?
return val; // no special processing required before passing to XML library
}
MyExports.encodeDateTime = function encodeDateTime(val)
{
// '2018-05-30T17:30:00'
// process undefined the same as null, or should we catch and return err_val?
if (val==null) return null; // could be nullable
if (val instanceof Date) return val.toISOString();
if (MyExports.IsString(val)) return val; // assume if string, it's just passing through
throw new Error('encodeDateTime requires instance of Date or a string');
}
MyExports.encodeTimeSpan = function encodeTimeSpan(val)
{
if (val==null) return null;
val = parseFloat(val); // in case it is not a number already (or could use Number() or '+' operator?)
if (!Number.isFinite(val)) throw new Error('encodeTimeSpan requires a number of seconds');
// Is this cheap? Yes. Yes it is.
return "PT" + val + "S"; // val assumed to be seconds
}
// endregion "Some parsers for primitive node values" -----
// region "Helper Functions" -----
MyExports.IsString = function IsString(val) { return (typeof(val)=='string' || val instanceof String); }
// Technically prototype and constructor should always be there?
MyExports.IsClassFunction = function IsClassFunction(val) { return (typeof(val) == 'function' && val.prototype); }
MyExports.IsClassInstance = function IsClassInstance(val) { return (typeof(val) == 'object' && val.constructor); }
/*MyExports.GuessClassName = function(v)
{
// Take a guess at what is most appropriate class name for this JS variable
var class_name;
if (typeof(v) == 'number' || v instanceof Number) class_name = 'double';
else if (MyExports.IsString(v)) class_name = 'string';
else if (MyExports.IsClassFunction(v)) class_name = v.name;
else if (MyExports.IsClassInstance(v)) class_name = v.constructor.name;
else return null;
return class_name;
}*/
function CheckConvertClassName(class_name)
{
// If passed the actual class function
if (MyExports.IsClassFunction(class_name)) class_name = class_name.name;
// if passed an instance of the class
else if (MyExports.IsClassInstance(class_name)) class_name = class_name.constructor.name;
// if passed one of our objects that holds a class name
else if (class_name instanceof XmlTemplateItem) class_name = class_name.ClassName;
else if (class_name instanceof XmlTemplate) class_name = class_name.getName(true);
return class_name;
}
function getShortClass(class_name)
{
if (class_name==null) return null;
var fields = class_name.split('.'); // remove any extra namespace qualifiers
return fields[fields.length-1];
}
MyExports.genArrLevels = function genArrLevels(levels, class_name, xml_mode)
{
if (levels==null) return null; // no levels
if (Array.isArray(levels)) return levels; // already an array of levels
if (!Number.isFinite(levels)) throw new Error('Array levels must be array of names of number of dimensions');
if (levels < 1) return null; // zero-dimensions is effectively no array
class_name = getShortClass(class_name); // drop any qualifier for use as tag name
var ArrLevels = [];
var str = (xml_mode==MyExports.xmlModes.DataContractSerializer ? class_name : class_name.charAt(0).toUpperCase() + class_name.slice(1));
// levels are named from inner dimension to outer dimensions
ArrLevels[0] = class_name;
for (let i=1; i < levels; ++i)
{
str = ('ArrayOf' + str);
ArrLevels[i] = str;
}
return ArrLevels;
}
/*
DataContract suffix hashes essentially derived from md5 of a special composite name spaces string?
https://github.com/mono/mono/blob/master/mcs/class/referencesource/System.Runtime.Serialization/System/Runtime/Serialization/DataContractSerializer.cs
https://github.com/mono/mono/blob/master/mcs/class/referencesource/System.Runtime.Serialization/System/Runtime/Serialization/DataContract.cs
https://github.com/mono/mono/blob/0bcbe39b148bb498742fc68416f8293ccd350fb6/mcs/class/referencesource/System.ServiceModel.Internals/System/Runtime/HashHelper.cs
var data = "asdf";
var crypto = require('crypto');
crypto.createHash('md5').update(data).digest("base64");
// TODO - is this a useful helper or just a big mess?
MyExports.genDictClassNameDc = function genDictClassNameDc(pair_class)
{
// KeyValueOf may contain namespace hash suffix, so better ask
return 'ArrayOf' + pair_class;
}
MyExports.genDictClassName = function genDictClassName(key_class, value_class, xml_mode)
{
key_class = CheckConvertClassName(key_class);
value_class = CheckConvertClassName(value_class);
if (xml_mode==MyExports.xmlModes.DataContractSerializer)
{
// TODO - actually you need the pair name since that may have a hash suffix
return 'ArrayOfKeyValueOf' + key_class + value_class;
}
else
{
var alias = MyExports.CsharpTypeAliases[key_class];
if (alias!=undefined) key_class = alias;
var alias = MyExports.CsharpTypeAliases[value_class];
if (alias!=undefined) value_class = alias;
return 'DictionaryOf' + key_class.charAt(0).toUpperCase() + key_class.slice(1)
+ value_class.charAt(0).toUpperCase() + value_class.slice(1);
}
} */
// endregion "Helper Functions" -----
// region "Constants and LUTs" -----
// typical XmlNameSpaces for DataContract
MyExports.xmlNS_Array = 'http://schemas.microsoft.com/2003/10/Serialization/Arrays';
MyExports.xmlNS_System = 'http://schemas.datacontract.org/2004/07/System';
MyExports.xmlNS_None = '';
MyExports.xmlModes = { XmlSerializer:0, DataContractSerializer:1 };
MyExports.CsharpTypeAliases =
{
// there is no Int8/UInt8, it just stays sbyte/byte or SByte/Byte
'sbyte': 'SByte',
'byte': 'Byte',
'int': 'Int32',
'uint': 'UInt32',
'short': 'Int16',
'ushort': 'UInt16',
'long': 'Int64',
'ulong': 'UInt64',
}
// endregion "Constants and LUTs" -----
// region "Generic object as Dictionary" -----
// In theory, one could create a dictionary as an array of a key-value classes and use serializer normally, BUT what about a generic JS object used as a dictionary?
/*
class KeyValuePair
{
constructor(k,v)
{
this.Key=k; this.Value=v;
}
static getXmlTemplate()
{
var temp = new xml_sharp.XmlTemplate(this);
temp.add('Key', ?); // whatever type Key stores
temp.add('Value', ?); // whatever type Value stores
return temp;
}
}
class SomeClass
{
constructor()
{
this.Dictionary = []; // array of KeyValuePair
}
static getXmlTemplate()
{
var temp = new xml_sharp.XmlTemplate(this);
temp.add('Dictionary', 'KeyValuePair', 1);
return temp;
}
}
*/
// For a {} dictionary we need to know what the key and value tags are named in the xml, and what class each is.
// (Key is most likely going to be a 'string', unless you want to try to use a JSON string for an object key?)
// ClassName is needed only if there is an array of dictionaries, otherwise the property name is used for the main node.
/*
<ClassName>
<PairName>
<KeyName>KeyClass</KeyName>
<ValueName>ValueClass</ValueName>
</PairName>
</ClassName>
*/
/**
* Internal stub for abstracting an array that is transparent in the XML structure.
* @private
* @class ArrayStub
*/
class ArrayStub
{
constructor()
{
this._Items_ = []; // this property should not show in XML
}
}
/**
* Internal stub for a key-value pair with arbitrary key/value names.
* @private
* @class KeyValuePairStub
*/
class KeyValuePairStub
{
constructor(key_name, key_content, value_name, value_content)
{
// actual exposed properties for serializer (Content cannot be undefined!)
if (key_content===undefined) key_content=null;
if (value_content===undefined) value_content=null;
this[key_name] = key_content;
this[value_name] = value_content;
}
}
/**
* Internal factory for producing dictionary stubs for abstracting generic object.
* @private
* @class DictionaryFactory
*/
class DictionaryFactory // For now maybe we don't expose internal plumbing of generic object dictionary
{
constructor(pair_name, key_prop, value_prop, namespace)
{
if (!MyExports.IsString(pair_name)) throw new Error('DictionaryFactory pair_name must be string');
if (!(key_prop instanceof XmlTemplateItem)) throw new Error('DictionaryFactory key_class must be XmlTemplateItem');
if (!(value_prop instanceof XmlTemplateItem)) throw new Error('DictionaryFactory value_class must be XmlTemplateItem');
// TODO - ACTUALLY for a generic {} dictionary, the key must be a string or representable as a string? At least for now.
// Actually any simple type that can be used as a js object prop name should work?
//if (key_prop.ClassName!='string') throw new Error('DictionaryFactory: At this time key_class must be "string"');
this.PairName = pair_name;
this.KeyProp = key_prop;
this.ValueProp = value_prop;
this.XmlNameSpace = namespace;
}
createDictTemplate(class_name)
{
var t = new XmlTemplate(ArrayStub);
var item = t.add('_Items_', this.PairName, 1, this.XmlNameSpace);
item.DictionaryData = this;
t.XmlPassthrough = '_Items_'; // in xml processing, this class is transparent, the value of Items replaces the class itself in XML
return t;
}
createPairTemplate(key, value)
{
// create a temporary template for this particular KeyValuePair
var args = [this.KeyProp.Name, key, this.ValueProp.Name, value];
var t = new XmlTemplate(KeyValuePairStub, args);
t.add(this.KeyProp); // TODO - do these need to be clones? is it safe to just pass the original props?
t.add(this.ValueProp);
return t;
}
createPairStub(key, value)
{
return new KeyValuePairStub(this.KeyProp.Name, key, this.ValueProp.Name, value);
}
} // END CLASS: DictionaryFactory
// endregion "Generic object as Dictionary" -----
/**
* Internal factory for handling potentially multidimensional arrays
* @private
* @class ArrayFactory
*/
class ArrayFactory
{
constructor(levels, namespace) // levels is either a number of dimensions or an array of string names
{
// allow user to defer on level names, but level names must be known during use!
if (!Array.isArray(levels) && !Number.isFinite(levels)) throw new Error('Array levels must be string[] or number');
this.Levels = levels;
this.XmlNameSpace = namespace;
}
clone(new_levels, new_namespace)
{
if (new_levels==undefined) new_levels = this.Levels;
if (new_namespace==undefined) new_namespace = this.XmlNameSpace;
return new ArrayFactory(new_levels, new_namespace);
}
getTopTag() { return this.Levels[this.Levels.length-1]; }
isOneDim() { return (this.Levels==null || this.Levels.length==1); }
nextTemp(cur_prop) // call in decode/encode to handle extra layer
{
var nx = new ArrayFactory(this.Levels.slice(0, -1), this.XmlNameSpace); // get next level
var t = new XmlTemplate(ArrayStub);
t.add( cur_prop.clone('_Items_', cur_prop.ClassName, nx) );
t.XmlPassthrough = '_Items_'; // in xml processing, this class is transparent, the value of Items replaces the class itself in XML
return t;
}
} // END CLASS: ArrayFactory
/** Class representing the XML template for a given property. */
class XmlTemplateItem
{
/**
* Creates an instance of XmlTemplateItem.
* @param {string} prop_name Property Name
* @param {string} class_name Class or Type Name
* @param {?string[]|number} [arr_levels=null] XML tag names for array levels or number of dimensions (if not defined, assumes not an array)
* @param {?string} [arr_namespace=undefined] XML namespace for array, if any
* @param {?boolean} [isNullable=false] If simple type should be flagged as nullable
* @param {?boolean} [hasExplicitTypeTag=false] If true this prop uses an explicit type tag (somewhat like an array without being one)
* @param {?booleab} [isFlatArray=false] If true and this prop is array, treat it as 'flat' or 'headless'
*/
constructor(prop_name, class_name, arr_levels, arr_namespace, isNullable, hasExplicitTypeTag, isFlatArray)
{
if (!MyExports.IsString(prop_name)) throw new Error('XmlTemplateItem.constructor prop_name must be string');
if (!MyExports.IsString(class_name)) throw new Error('XmlTemplateItem.constructor class_name must be string');
//if (class_name=='') throw new Error("XmlTemplateItem.constructor class_name cannot be empty string");
this.Name = prop_name;
this.ClassName = class_name;
// instance of ArrayFactory
if (!arr_levels) this.ArrayData = null;
else if (arr_levels instanceof ArrayFactory) this.ArrayData = arr_levels;
else this.ArrayData = new ArrayFactory(arr_levels, arr_namespace);
this.NullableData = (isNullable ? {} : null);
this.AttrData = null; // Add a member for tracking XML element vs attribute
this.ExplicitTypeTag = (hasExplicitTypeTag ? {} : null);
this.FlatArray = (isFlatArray ? {} : null);
// temp runtime props
//this.DictionaryData = null; // can hold a DictionaryFactory in a KeyValuePair during XML processing
}
_checkArray(opts)
{
// if Array Levels already set, nothing to do
if (Array.isArray(this.ArrayData.Levels)) return this;
// else we need to generate a copy with auto generated array names
var n = this.clone();
n.ArrayData.Levels = MyExports.genArrLevels(this.ArrayData.Levels, this.ClassName, opts.XmlMode);
return n;
}
clone(prop_name, class_name, arr_levels)
{
if (prop_name==undefined) prop_name = this.Name;
if (class_name==undefined) class_name = this.ClassName;
// arr_namespace is part of ArrayData
if (arr_levels==undefined) arr_levels = this.ArrayData.clone(); // no override, create deep copy
else if (Array.isArray(arr_levels)) arr_levels = this.ArrayData.clone(arr_levels); // create copy with new levels
var n = new XmlTemplateItem(prop_name, class_name, arr_levels);
n.NullableData = this.NullableData;
n.AttrData = this.AttrData;
n.DictionaryData = this.DictionaryData; // some temporary props carry dict data
n.ExplicitTypeTag = this.ExplicitTypeTag;
n.FlatArray = this.FlatArray;
return n;
}
/**
* Mark XmlTemplateItem as nullable
* @returns {XmlTemplateItem} This XmlTemplateItem instance
*/
nullable()
{
if (this.AttrData) throw new Error("Nullable types not supported as XML attributes")
this.NullableData = {}; // could contain other options if needed?
return this;
}
/**
* Mark XmlTemplateItem as an XML attribute
* @returns {XmlTemplateItem} This XmlTemplateItem instance
*/
attr()
{
if (this.ArrayData) throw new Error("Arrays not supported as XML attributes");
if (this.NullableData) throw new Error("Nullable types not supported as XML attributes")
if (this.ExplicitTypeTag) throw new Error("Explicit type tag not supported as XML attributes")
// mark prop as an attribute? does it need to be a primitive or just call toString/ValueOf? Rely on given type decode/encode functions?
this.AttrData = {};
return this;
}
/**
* Mark XmlTemplateItem as having an explicit type tag
* @returns {XmlTemplateItem} This XmlTemplateItem instance
*/
explicitTypeTag()
{
if (this.AttrData) throw new Error("Explicit type tag not supported as XML attributes")
this.ExplicitTypeTag = {};
return this;
}
/**
* Mark XmlTemplateItem as having a flat or headless XML array
* @returns {XmlTemplateItem} This XmlTemplateItem instance
*/
flatArr()
{
this.FlatArray = {};
return this;
}
} // END CLASS: XmlTemplateItem
MyExports.XmlTemplateItem = XmlTemplateItem;
/** The XmlTemplate class stores info of how a class's properties are to be serialized. */
class XmlTemplate
{
/**
* Creates an instance of XmlTemplate.
* @param {Function} class_constructor Class function (essentially the constructor)
* @param {?any[]} [constructor_args=null] Arguments to feed constructor when creating a new instance
* @param {?string} [class_name=null] An alternative class name or alias to use in place of the constructor's name
*/
constructor(class_constructor, constructor_args, class_name)
{
this.extend(class_constructor, constructor_args, class_name);
this.Props = []; // array of XmlTemplateItem
this.XmlNameSpace = null;
// temp runtime props
//this.XmlPassthrough = null; // name of prop that is used as abstract passthrough for XML processing
}
/**
* Gets the name of the class being mapped by this XML template.
* @param {?boolean} [full=false] If true, include any class name qualifiers
* @return {string} Name of the class that this template maps
*/
getName(full) { return (full ? this.ClassName : getShortClass(this.ClassName)); }
/**
* Checks if this template is using a class name alias
* @returns {boolean} True if this template uses a class name alias
*/
hasAlias() { return (this.ClassConstructor.name!=this.ClassName); }
/**
* Converts the current template into a template for the given derived class
* @param {Function} class_constructor Class function (essentially the constructor)
* @param {?any[]} [constructor_args=null] Arguments to feed constructor when creating a new instance
* @param {?string} [class_name=null] An alternative class name to use in place of the constructor's name
* @return {XmlTemplate} The current modified template (not a copy)
*/
extend(class_constructor, constructor_args, class_name)
{
if (!MyExports.IsClassFunction(class_constructor)) throw new Error('XmlTemplate.constructor requires class_constructor to be class function');
if (constructor_args!=null && !Array.isArray(constructor_args)) throw new Error('XmlTemplate.constructor requires constructor_args to be null or an array');
// mark any existing Props as inherited? might be useful later?
if (this.ClassName && Array.isArray(this.Props))
{
this.Props.forEach(function(item)
{
if (Array.isArray(item.BaseClass)) item.BaseClass.push(this.ClassName); // just keep track of last, or track the whole stack up?
else item.BaseClass = [this.ClassName];
},this);
}
this.ClassConstructor = class_constructor;
this.ConstructorArgs = constructor_args || null;
this.ClassName = (class_name ? class_name : this.ClassConstructor.name);
return this; // should we just return this for possible chaining ?
}
/**
* Makes a shallow copy of this template. You can specify a different class or constructor args if you want.
* @param {Function} [class_constructor] Class function (essentially the constructor)
* @param {?any[]} [constructor_args] Arguments to feed constructor when creating a new instance
* @param {?string} [class_name] An alternative class name to use in place of the constructor's name
* @return {XmlTemplate} The shallow clone of this
*/
clone(class_constructor, constructor_args, class_name)
{
if (class_constructor==undefined) class_constructor = this.ClassConstructor;
if (constructor_args==undefined) constructor_args = this.ConstructorArgs; // shallow copy? since it's arbitary, deep copy might be a bit much?
if (class_name==undefined) class_name = this.ClassName;
var n = new XmlTemplate(class_constructor, constructor_args, class_name);
n.Props = this.Props.slice(); // shallow copy good enough?
//this.Props.forEach(function(item){ n.Props.push(item.clone()) }); // deep copy safer?
n.XmlNameSpace = this.XmlNameSpace;
return n;
}
/**
* Returns a new instance of the class associated with this template.
* @param {...any} constructor_args If parameters given, they are passed to constructor; otherwise any stored ConstructorArgs are used.
* @returns {Object} New instance of ClassConstructor (using ConstructorArgs if any)
*/
newObj(...constructor_args)
{
// NOTE: explicitly passing undefined counts as arguments.length==1 which would override ConstructorArgs if needed.
if (constructor_args.length>0) return new this.ClassConstructor(...constructor_args);
if (Array.isArray(this.ConstructorArgs)) return new this.ClassConstructor(...this.ConstructorArgs);
return new this.ClassConstructor();
}
/**
* Add property to this class XML template.
* @param {string|XmlTemplateItem} prop_name Property Name or instance of Property XML template. If passing full item template, other parameters are ignored.
* @param {string|Function|Object} class_name Class Name or Class instance or Class function of the property.
* @param {?number|string[]} [arr_levels=0] Number of dimensions or array of tag names (if not defined, assumes no array)
* @param {?string} [arr_namespace=undefined] XML namespace for array, if any
* @param {?boolean} [isNullable=false] If simple type should be flagged as nullable
* @param {?boolean} [hasExplicitTypeTag=false] If true this prop uses an explicit type tag (somewhat like an array without being one)
* @param {?booleab} [isFlatArray=false] If true and this prop is array, treat it as 'flat' or 'headless'
* @returns {XmlTemplateItem} Instance of the new XML template item that was added for this property
*/
add(prop_name, class_name, arr_levels, arr_namespace, isNullable, hasExplicitTypeTag, isFlatArray)
{
var obj = prop_name; // allow feeding just a XmlTemplateItem instance
if (!(obj instanceof XmlTemplateItem))
{
class_name = CheckConvertClassName(class_name);
obj = new XmlTemplateItem(prop_name, class_name, arr_levels, arr_namespace, isNullable, hasExplicitTypeTag, isFlatArray);
}
this.Props.push(obj);
return obj; // return XmlTemplateItem in case it is useful at some point?
}
// common type add helpers
addBool(prop_name, ...args) {return this.add(prop_name,'bool',...args)}
addString(prop_name, ...args) {return this.add(prop_name,'string',...args)}
addSByte(prop_name, ...args) {return this.add(prop_name,'sbyte',...args)}
addByte(prop_name, ...args) {return this.add(prop_name,'byte',...args)}
addInt(prop_name, ...args) {return this.add(prop_name,'int',...args)}
addUInt(prop_name, ...args) {return this.add(prop_name,'uint',...args)}
addShort(prop_name, ...args) {return this.add(prop_name,'short',...args)}
addUShort(prop_name, ...args) {return this.add(prop_name,'ushort',...args)}
addLong(prop_name, ...args) {return this.add(prop_name,'long',...args)}
addULong(prop_name, ...args) {return this.add(prop_name,'ulong',...args)}
addFloat(prop_name, ...args) {return this.add(prop_name,'float',...args)}
addDouble(prop_name, ...args) {return this.add(prop_name,'double',...args)}
addDateTime(prop_name, ...args) {return this.add(prop_name,'DateTime',...args)}
addTimeSpan(prop_name, ...args) {return this.add(prop_name,'TimeSpan',...args)}
addInt16(prop_name, ...args) {return this.add(prop_name,'Int16',...args)}
addUInt16(prop_name, ...args) {return this.add(prop_name,'UInt16',...args)}
addInt32(prop_name, ...args) {return this.add(prop_name,'Int32',...args)}
addUInt32(prop_name, ...args) {return this.add(prop_name,'UInt32',...args)}
addInt64(prop_name, ...args) {return this.add(prop_name,'Int64',...args)}
addUInt64(prop_name, ...args) {return this.add(prop_name,'UInt64',...args)}
/*addAuto(prop) // if props have an initial value, could we automatically determine class names? Could be problematic?
{
var class_name = GuessClassName(prop);
var prop_name = ; // need to either determine original name or pass separately?
this.add(prop_name, class_name);
}*/
/**
* Sorts the properties list by property names
* @param {?boolean} [skip_inherited=false] If true, any inherited props are ignored and put at top of the list in the order they are encounted.
* @returns {XmlTemplate} This instance
*/
sortByName(skip_inherited)
{
if (skip_inherited)
{
var inherited = [];
var other = [];
this.Props.forEach(function(item){ if (item.BaseClass) inherited.push(item); else other.push(item); });
other.sort(function(a,b) { return (a.Name<b.Name ? -1 : (a.Name>b.Name ? 1 : 0)); });
this.Props = inherited.concat(other);
}
else this.Props.sort(function(a,b) { return (a.Name<b.Name ? -1 : (a.Name>b.Name ? 1 : 0)); });
return this;
}
/**
* Sets the XML namespace for this class template
* @param {string} xml_namespace The full XML namespace to use for this class
* @returns {XmlTemplate} This instance
*/
setXmlNameSpace(xml_namespace) { this.XmlNameSpace = xml_namespace; return this; }
// parsing/generating methods for various kinds of XML library objects
/**
* Deserializes the class described by this template from the given XML node
* @private
* @param {Object} xml_obj Current XML node
* @param {Object} opts Options to be used during the deserialization process
* @param {Object} _state Internal state information
* @return {Object} Instance of the class described by this template resulting from the XML
*/
_from_xmlobj(xml_obj, opts, _state)
{
if (xml_obj==null) return null;
var new_obj = this.newObj();
this.Props.forEach(function(prop)
{
try
{
_state.pushPath(this, prop);
var xml_node;
if (this.XmlPassthrough || prop.AttrData || prop.FlatArray) xml_node = xml_obj;
else xml_node = xml_obj.getFirstNode(_state.prefix(prop.Name));
if (xml_node!=undefined) // XML has this class property
{
var prevNS = _state.saveNsState();
var new_item = undefined; // null is a valid answer, so use undefined until it is defined
if (xml_node.getAttr(_state.XmlInstance+':nil')=='true') // regardless of being nullable, if tagged nil make it null
{
new_obj[prop.Name] = null;
return;
}
var ns = _state.Factory._findNS(prop);
if (ns != null)
{
if (ns=='' || ns == _state.RootNameSpace) _state.setPrefix(ns, null, opts);
else
{
let pns = xml_node.findNS(ns);
_state.setPrefix(ns, pns, opts);
}
}
if (prop.ArrayData) // we expect an array of a single type
{
prop = prop._checkArray(opts);
var node_arr = xml_node.getNodes(_state.prefix(prop.ArrayData.getTopTag()));
if (node_arr==undefined)
{
if (this.XmlPassthrough==prop.Name) new_obj[prop.Name] = null;
else new_obj[prop.Name] = []; // tag was there but empty, so empty array?
return;
}
new_item = [];
node_arr.forEach(function(item,index)
{
_state.ObjPath.push(index);
var temp = _state.Factory._decodeType(item, prop, opts, _state);
if (temp!==undefined) new_item.push(temp); // null is a valid answer
_state.ObjPath.pop();
}, this);
new_obj[prop.Name] = new_item;
}
else // we expect a single value
{
if (prop.ExplicitTypeTag) xml_node = xml_node.getFirstNode(prop.ClassName); // unwrap extra type tag
new_item = _state.Factory._decodeType(xml_node, prop, opts, _state);
if (new_item!==undefined) new_obj[prop.Name] = new_item; // null is a valid answer
}
_state.loadNsState(prevNS);
}
//else new_obj[prop.Name] = null; // XML does not have this prop, so it must be null? Actually value should be unchanged.
}
catch (e)
{
if (e instanceof XmlSerializerError) throw e;
else throw new XmlSerializerError(e, opts, _state);
}
finally { _state.popAll(); } // in case of a normal return from try block (after catch state is useless)
}, this);
return new_obj;
}
/**
*
* Serializes this class to a XML node
* @private
* @param {Object} class_inst Instance of the class described by this template from which to produce XML
* @param {Object} xml_obj Current XML node
* @param {Object} opts Options to be used during the deserialization process
* @param {Object} _state Internal state information
* @return {Object} XML object describing the resulting XML node (TODO - this return isn't really used?)
*/
_to_xmlobj(class_inst, xml_obj, opts, _state)
{
if (class_inst==null) throw new Error('XmlTemplate._to_xmlobj given null object');
this.Props.forEach(function(prop)
{
try
{
var new_item = (this.XmlPassthrough || prop.FlatArray ? xml_obj : xml_obj.makeNode(_state.prefix(prop.Name)));
_state.pushPath(this, prop);
var prevNS = _state.saveNsState();
var ns = _state.applyNS(_state.Factory._findNS(prop), opts);
var ns_prefix = _state.checkForNsChange(prevNS, ns);
if (ns_prefix) new_item.addAttr('xmlns:'+ns_prefix, ns);
var cur_data = class_inst[prop.Name];
if (cur_data==null)
{
if ((prop.NullableData && prop.ArrayData==null) || opts.UseNil )
{
new_item.addAttr(_state.XmlInstance+':nil', 'true');
if (xml_obj!==new_item) xml_obj.addNode(new_item);
}
}
else if (prop.ArrayData)
{
prop = prop._checkArray(opts);
var level = _state.prefix(prop.ArrayData.getTopTag());
if (!Array.isArray(cur_data)) cur_data = [cur_data];
cur_data.forEach(function(item,index)
{
_state.ObjPath.push(index);
var arr_item = new_item.makeNode(level);
arr_item = _state.Factory._encodeType(item, prop, arr_item, opts, _state);
if (arr_item===undefined) throw new Error('XmlTemplate._to_xmlobj could not generate "' + prop.ClassName +'"');
new_item.addNode(arr_item);
_state.ObjPath.pop();
},this);
if (xml_obj!==new_item) xml_obj.addNode(new_item);
}
else if (prop.ExplicitTypeTag) // it's like being in an unlisted array of one
{
var arr_item = new_item.makeNode(prop.ClassName);
arr_item = _state.Factory._encodeType(cur_data, prop, arr_item, opts, _state);
if (arr_item===undefined) throw new Error('XmlTemplate._to_xmlobj could not generate "' + prop.ClassName +'"');
new_item.addNode(arr_item);
if (xml_obj!==new_item) xml_obj.addNode(new_item);
}
else // single node is pretty straight forward for xml2js
{
new_item = _state.Factory._encodeType(cur_data, prop, new_item, opts, _state);
if (new_item===undefined) throw new Error('XmlTemplate._to_xmlobj could not generate "' + prop.ClassName+'"');
if (prop.AttrData) xml_obj.addAttr(prop.Name, new_item.getValue());
else if (xml_obj!==new_item) xml_obj.addNode(new_item);
}
_state.loadNsState(prevNS);
}
catch (e)
{
if (e instanceof XmlSerializerError) throw e;
else throw new XmlSerializerError(e, opts, _state);
}
finally { _state.popAll(); } // in case of a normal return from try block (after catch state is useless)
}, this);
return xml_obj;
}
} // END CLASS: XmlTemplate
MyExports.XmlTemplate = XmlTemplate;
/** The XmlTemplateFactory class stores a collection of XmlTemplate instances for serializing them into/out-of XML. */
class XmlTemplateFactory
{
/**
* Creates an instance of XmlTemplateFactory.
* @param {...Function|XmlTemplate} [templates] Variable number of class functions (with static getXmlTemplate), or XmlTemplate's, or arrays of either.
*/
constructor(...templates)
{
this.XmlTemplates = {}; // use object instead of array so we can use class names as unique keys
this.Enums = {}; // Enumerations
this.ImplicitDicts = {}; // Implicit object dictionaries
this.ClassNameAlias = {}; // Class name aliases
// allow passing list of classes to constructor? both as variable param list or as an array?
templates.forEach(function(arg)
{
if (Array.isArray(arg))
{
arg.forEach(function(a)
{
this.add(a);
},this);
}
else this.add(arg);
},this);
// Allow each factory to hold its own simple type handlers so they can be customized per factory
this.SimpleTypeDecoders =
{
'bool': MyExports.decodeBool,
'string': MyExports.decodeString,
'sbyte': MyExports.decodeInt,
'byte': MyExports.decodeInt,
'short': MyExports.decodeInt, 'Int16': MyExports.decodeInt,
'ushort': MyExports.decodeInt, 'UInt16': MyExports.decodeInt,
'int': MyExports.decodeInt, 'Int32': MyExports.decodeInt,
'uint': MyExports.decodeInt, 'UInt32': MyExports.decodeInt,
'long': MyExports.decodeString, 'Int64': MyExports.decodeString,
'ulong': MyExports.decodeString, 'UInt64': MyExports.decodeString,
'float': MyExports.decodeFloat,
'double': MyExports.decodeDouble,
'DateTime': MyExports.decodeDateTime,
'TimeSpan': MyExports.decodeTimeSpan,
};
this.SimpleTypeEncoders =
{
'bool': MyExports.encodeBool,
'string': MyExports.encodeString,
'sbyte': MyExports.encodePassthrough,
'byte': MyExports.encodePassthrough,
'short': MyExports.encodePassthrough, 'Int16': MyExports.encodePassthrough,
'ushort': MyExports.encodePassthrough, 'UInt16': MyExports.encodePassthrough,
'int': MyExports.encodePassthrough, 'Int32': MyExports.encodePassthrough,
'uint': MyExports.encodePassthrough, 'UInt32': MyExports.encodePassthrough,
'long': MyExports.encodeString, 'Int64': MyExports.encodeString,
'ulong': MyExports.encodeString, 'UInt64': MyExports.encodeString,
'float': MyExports.encodePassthrough,
'double': MyExports.encodePassthrough,
'DateTime': MyExports.encodeDateTime,
'TimeSpan': MyExports.encodeTimeSpan,
};
this.SimpleTypeNameSpaces = {}; // defaults are all buit-in and have no namespace
/* this.SimpleTypeMeta =
{
'bool': { BuiltIn: true },
'string': { BuiltIn: true },
'sbyte': { BuiltIn: true },
'byte': { BuiltIn: true },
'short': { BuiltIn: true }, 'Int16': { BuiltIn: true },
'ushort': { BuiltIn: true }, 'UInt16': { BuiltIn: true },
'int': { BuiltIn: true }, 'Int32': { BuiltIn: true },
'uint': { BuiltIn: true }, 'UInt32': { BuiltIn: true },
'long': { BuiltIn: true }, 'Int64': { BuiltIn: true },
'ulong': { BuiltIn: true }, 'UInt64': { BuiltIn: true },
'float': { BuiltIn: true },
'double': { BuiltIn: true },
'DateTime': { BuiltIn: true },
'TimeSpan': { BuiltIn: true },
}; */
}
/**
* Sets the given simple type decoder or encoder for this factory
* @param {string|string[]} type_names Simple type name(s) being set
* @param {?DecoderCallback} [decode_func=null] Function to decode XML node string into JS property value
* @param {?EncoderCallback} [encode_func=null] Function to encode JS property value into XML node string
* @param {?string} [type_namespace=null] XML namespace to use for this simple type
* @return {XmlTemplateFactory} This factory instance
*/
setSimpleCodec(type_names, decode_func, encode_func, type_namespace)
{
if (decode_func!=null && typeof(decode_func)!='function') throw new Error('decode_func is not a function');
if (encode_func!=null && typeof(encode_func)!='function') throw new Error('encode_func is not a function');
if (!Array.isArray(type_names)) type_names = [type_names];
type_names.forEach(function(item)
{
if (decode_func!=null) this.SimpleTypeDecoders[item] = decode_func;
if (encode_func!=null) this.SimpleTypeEncoders[item] = encode_func;