jspurefix 5.3.0 → 5.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/BACKPORT_PLAN.md +120 -10
  2. package/DEMO_PORT_PLAN.md +286 -0
  3. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +39 -0
  4. package/dist/dictionary/parser/quickfix/dictionary-validator.js +321 -0
  5. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -0
  6. package/dist/dictionary/parser/quickfix/index-visitor.d.ts +10 -0
  7. package/dist/dictionary/parser/quickfix/index-visitor.js +68 -0
  8. package/dist/dictionary/parser/quickfix/index-visitor.js.map +1 -0
  9. package/dist/dictionary/parser/quickfix/index.d.ts +7 -0
  10. package/dist/dictionary/parser/quickfix/index.js +7 -0
  11. package/dist/dictionary/parser/quickfix/index.js.map +1 -1
  12. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.d.ts +13 -0
  13. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js +65 -0
  14. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js.map +1 -0
  15. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +73 -0
  16. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +363 -0
  17. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -0
  18. package/dist/dictionary/parser/quickfix/sax-tree-builder.d.ts +5 -0
  19. package/dist/dictionary/parser/quickfix/sax-tree-builder.js +103 -0
  20. package/dist/dictionary/parser/quickfix/sax-tree-builder.js.map +1 -0
  21. package/dist/dictionary/parser/quickfix/validation-error.d.ts +17 -0
  22. package/dist/dictionary/parser/quickfix/validation-error.js +32 -0
  23. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -0
  24. package/dist/dictionary/parser/quickfix/x-element.d.ts +26 -0
  25. package/dist/dictionary/parser/quickfix/x-element.js +82 -0
  26. package/dist/dictionary/parser/quickfix/x-element.js.map +1 -0
  27. package/dist/store/fix-msg-ascii-store-resend.js +6 -0
  28. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  29. package/dist/store/store-config.d.ts +1 -0
  30. package/dist/store/store-config.js.map +1 -1
  31. package/dist/transport/ascii/ascii-session.js +3 -0
  32. package/dist/transport/ascii/ascii-session.js.map +1 -1
  33. package/dist/util/definition-factory.js +1 -1
  34. package/dist/util/definition-factory.js.map +1 -1
  35. package/jsfix.test_client.txt +67 -67
  36. package/jsfix.test_server.txt +64 -64
  37. package/package.json +6 -6
  38. package/src/dictionary/parser/quickfix/dictionary-validator.ts +473 -0
  39. package/src/dictionary/parser/quickfix/index-visitor.ts +100 -0
  40. package/src/dictionary/parser/quickfix/index.ts +7 -0
  41. package/src/dictionary/parser/quickfix/quick-fix-graph-file-parser.ts +63 -0
  42. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +450 -0
  43. package/src/dictionary/parser/quickfix/sax-tree-builder.ts +112 -0
  44. package/src/dictionary/parser/quickfix/validation-error.ts +34 -0
  45. package/src/dictionary/parser/quickfix/x-element.ts +115 -0
  46. package/src/store/fix-msg-ascii-store-resend.ts +8 -0
  47. package/src/store/store-config.ts +1 -0
  48. package/src/transport/ascii/ascii-session.ts +4 -0
  49. package/src/util/definition-factory.ts +2 -2
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Graph-based QuickFix XML dictionary parser.
3
+ *
4
+ * Verbatim port of C# QuickFixXmlFileParser. Uses XDocument tree (from SaxTreeBuilder)
5
+ * for random-access XML traversal and a Node/Edge/work-queue pattern for forward
6
+ * reference resolution. Replaces the iterative N-pass SAX-streaming approach.
7
+ *
8
+ * Pre-parse validation via DictionaryValidator catches missing fields, duplicates,
9
+ * and undefined references with "did you mean" suggestions before parsing begins.
10
+ */
11
+ import { XDocument, XNode } from './x-element'
12
+ import { SaxTreeBuilder } from './sax-tree-builder'
13
+ import { DictionaryValidator } from './dictionary-validator'
14
+ import { FixDefinitions } from '../../definition/fix-definitions'
15
+ import { SimpleFieldDefinition } from '../../definition/simple-field-definition'
16
+ import { ComponentFieldDefinition } from '../../definition/component-field-definition'
17
+ import { GroupFieldDefinition } from '../../definition/group-field-definition'
18
+ import { MessageDefinition } from '../../definition/message-definition'
19
+ import { ContainedFieldSet } from '../../contained/contained-field-set'
20
+ import { ContainedSimpleField } from '../../contained/contained-simple-field'
21
+ import { ContainedComponentField } from '../../contained/contained-component-field'
22
+ import { ContainedGroupField } from '../../contained/contained-group-field'
23
+ import { ContainedSetBuilder } from '../../contained/contained-set-builder'
24
+ import { FixDefinitionSource } from '../../fix-definition-source'
25
+ import { VersionUtil } from '../../version-util'
26
+ import { IndexVisitor } from './index-visitor'
27
+
28
+ export enum NodeElementType {
29
+ MessageDefinition = 'MessageDefinition',
30
+ SimpleFieldDefinition = 'SimpleFieldDefinition',
31
+ SimpleFieldDeclaration = 'SimpleFieldDeclaration',
32
+ InlineGroupDefinition = 'InlineGroupDefinition',
33
+ GroupDefinition = 'GroupDefinition',
34
+ GroupDeclaration = 'GroupDeclaration',
35
+ ComponentDefinition = 'ComponentDefinition',
36
+ ComponentDeclaration = 'ComponentDeclaration'
37
+ }
38
+
39
+ export interface Edge {
40
+ readonly head: number
41
+ readonly tail: number
42
+ }
43
+
44
+ export class GraphNode {
45
+ private readonly _edges: Edge[] = []
46
+
47
+ constructor (
48
+ public readonly id: number,
49
+ public readonly name: string,
50
+ public readonly type: NodeElementType,
51
+ public readonly element: XNode
52
+ ) {}
53
+
54
+ get edges (): ReadonlyArray<Edge> {
55
+ return this._edges
56
+ }
57
+
58
+ makeEdge (tail: number): Edge {
59
+ const edge: Edge = { head: this.id, tail }
60
+ this._edges.push(edge)
61
+ return edge
62
+ }
63
+
64
+ isRequired (): boolean {
65
+ if (this.name === 'StandardHeader' || this.name === 'StandardTrailer') return true
66
+ return this.element.attribute('required') === 'Y'
67
+ }
68
+
69
+ toString (): string {
70
+ return `Node: id=${this.id}, name=${this.name}, type=${this.type}`
71
+ }
72
+ }
73
+
74
+ export interface QuickFixGraphParserOptions {
75
+ validateBeforeParsing?: boolean
76
+ }
77
+
78
+ export class QuickFixGraphParser {
79
+ private readonly definitions: FixDefinitions
80
+ private readonly nodes = new Map<number, GraphNode>()
81
+ private readonly containedSets = new Map<number, ContainedFieldSet>()
82
+ private readonly queue: GraphNode[] = []
83
+ private nextId = 0
84
+ private header: GraphNode | null = null
85
+ private trailer: GraphNode | null = null
86
+
87
+ public validator: DictionaryValidator | null = null
88
+ public readonly validateBeforeParsing: boolean
89
+
90
+ constructor (definitions: FixDefinitions, options: QuickFixGraphParserOptions = {}) {
91
+ this.definitions = definitions
92
+ this.validateBeforeParsing = options.validateBeforeParsing ?? true
93
+ }
94
+
95
+ /**
96
+ * Parse XML text into the FixDefinitions provided at construction.
97
+ * Throws DictionaryValidationException if validation is enabled and errors are found.
98
+ */
99
+ parseText (xml: string): FixDefinitions {
100
+ const doc = SaxTreeBuilder.parse(xml)
101
+ return this.parseDocument(doc)
102
+ }
103
+
104
+ /**
105
+ * Parse a pre-built XDocument tree.
106
+ */
107
+ parseDocument (doc: XDocument): FixDefinitions {
108
+ if (this.validateBeforeParsing) {
109
+ this.validator = new DictionaryValidator()
110
+ this.validator.validate(doc)
111
+ this.validator.throwIfErrors()
112
+ }
113
+
114
+ this.parseVersion(doc)
115
+ this.parseFields(doc)
116
+ this.parseComponents(doc)
117
+ this.parseHeader(doc)
118
+ this.parseTrailer(doc)
119
+ this.parseMessages(doc)
120
+
121
+ while (this.queue.length > 0) {
122
+ const node = this.queue.shift()!
123
+ this.work(node)
124
+ }
125
+
126
+ /*
127
+ * At this point all fields on all sets are placed, however the parent (e.g. an
128
+ * Instrument component) is not aware of the tags contained in nested groups/components
129
+ * that were resolved AFTER it. The IndexVisitor walks every message and re-indexes
130
+ * its tree post-order so each set knows all tags below it — essential for the segment
131
+ * parser to work.
132
+ */
133
+ new IndexVisitor().compute(this.definitions)
134
+
135
+ return this.definitions
136
+ }
137
+
138
+ // ── Graph construction ──
139
+
140
+ private makeNode (name: string, element: XNode, type: NodeElementType): GraphNode {
141
+ const node = new GraphNode(this.nextId++, name, type, element)
142
+ this.nodes.set(node.id, node)
143
+ this.queue.push(node)
144
+ return node
145
+ }
146
+
147
+ private constructTailNode (name: string, headNode: GraphNode, element: XNode, type: NodeElementType): void {
148
+ const tailNode = this.makeNode(name, element, type)
149
+ headNode.makeEdge(tailNode.id)
150
+ tailNode.makeEdge(headNode.id)
151
+ }
152
+
153
+ // ── Parsing entry points ──
154
+
155
+ private parseVersion (doc: XDocument): void {
156
+ const version = doc.firstDescendant('fix')
157
+ if (!version) throw new Error('no <fix> root element')
158
+ const major = parseInt(version.attribute('major') ?? '0', 10)
159
+ const minor = parseInt(version.attribute('minor') ?? '0', 10)
160
+ const servicepack = parseInt(version.attribute('servicepack') ?? '0', 10)
161
+ const description = (major !== 5 || servicepack === 0)
162
+ ? `FIX.${major}.${minor}`
163
+ : `FIX.${major}.${minor}SP${servicepack}`
164
+ const resolved = VersionUtil.resolve(description)
165
+ // FixDefinitions doesn't have a setVersion — version is set at construction
166
+ if (resolved !== this.definitions.version) {
167
+ throw new Error(`version mismatch: dictionary declares ${description} but FixDefinitions was constructed with ${this.definitions.version}`)
168
+ }
169
+ }
170
+
171
+ private parseFields (doc: XDocument): void {
172
+ const fieldsNode = doc.firstDescendant('fields')
173
+ if (!fieldsNode) return
174
+ for (const fieldElement of fieldsNode.elements('field')) {
175
+ this.makeNode(QuickFixGraphParser.nameFrom(fieldElement), fieldElement, NodeElementType.SimpleFieldDefinition)
176
+ }
177
+ }
178
+
179
+ private parseComponents (doc: XDocument): void {
180
+ const componentsNode = doc.firstDescendant('components')
181
+ if (!componentsNode) return
182
+ for (const componentElement of componentsNode.elements('component')) {
183
+ this.makeNode(QuickFixGraphParser.nameFrom(componentElement), componentElement, NodeElementType.ComponentDefinition)
184
+ }
185
+ }
186
+
187
+ private parseHeader (doc: XDocument): void {
188
+ const header = doc.firstDescendant('header')
189
+ if (!header) throw new Error('no header declared in fix definitions')
190
+ this.header = this.makeNode('StandardHeader', header, NodeElementType.ComponentDefinition)
191
+ }
192
+
193
+ private parseTrailer (doc: XDocument): void {
194
+ const trailer = doc.firstDescendant('trailer')
195
+ if (!trailer) throw new Error('no trailer declared in fix definitions')
196
+ this.trailer = this.makeNode('StandardTrailer', trailer, NodeElementType.ComponentDefinition)
197
+ }
198
+
199
+ private parseMessages (doc: XDocument): void {
200
+ const messagesNode = doc.firstDescendant('messages')
201
+ if (!messagesNode) return
202
+ for (const messageElement of messagesNode.elements('message')) {
203
+ const msgType = messageElement.attribute('msgtype')
204
+ if (!msgType) continue
205
+ this.makeNode(msgType, messageElement, NodeElementType.MessageDefinition)
206
+ }
207
+ }
208
+
209
+ // ── Work queue dispatch ──
210
+
211
+ private work (node: GraphNode): void {
212
+ switch (node.type) {
213
+ case NodeElementType.SimpleFieldDefinition: {
214
+ const sd = QuickFixGraphParser.getField(node.element)
215
+ this.definitions.addSimpleFieldDef(sd)
216
+ break
217
+ }
218
+ case NodeElementType.MessageDefinition: {
219
+ this.messageDefinition(node)
220
+ break
221
+ }
222
+ case NodeElementType.ComponentDefinition: {
223
+ this.componentDefinition(node)
224
+ break
225
+ }
226
+ case NodeElementType.SimpleFieldDeclaration: {
227
+ this.simpleFieldDeclaration(node)
228
+ break
229
+ }
230
+ case NodeElementType.InlineGroupDefinition: {
231
+ this.inlineGroupDefinition(node)
232
+ break
233
+ }
234
+ case NodeElementType.GroupDefinition: {
235
+ this.groupDefinition(node)
236
+ break
237
+ }
238
+ case NodeElementType.ComponentDeclaration: {
239
+ this.componentDeclaration(node)
240
+ break
241
+ }
242
+ case NodeElementType.GroupDeclaration: {
243
+ // QuickFix XML never has standalone group declarations — groups are always inline
244
+ break
245
+ }
246
+ }
247
+ }
248
+
249
+ // ── Definition handlers ──
250
+
251
+ private getComponentDefinition (node: GraphNode): ComponentFieldDefinition {
252
+ let definition = this.definitions.component.get(node.name)
253
+ if (definition) return definition
254
+ definition = new ComponentFieldDefinition(node.name, node.name, null, node.name)
255
+ this.definitions.addComponentFieldDef(definition)
256
+ return definition
257
+ }
258
+
259
+ private componentDeclaration (node: GraphNode): void {
260
+ const definition = this.getComponentDefinition(node)
261
+ const edge = node.edges[0]
262
+ const parentSet = this.containedSets.get(edge.tail)
263
+ if (!parentSet) {
264
+ throw new Error(`edge tail ${edge.tail} has no contained set on which to place declared component '${node.name}'`)
265
+ }
266
+ const containedComponent = new ContainedComponentField(definition, parentSet.fields.length, node.isRequired())
267
+ new ContainedSetBuilder(parentSet).add(containedComponent)
268
+ this.containedSets.set(edge.head, definition)
269
+ }
270
+
271
+ private messageDefinition (node: GraphNode): void {
272
+ if (!this.header) throw new Error('header not set')
273
+ if (!this.trailer) throw new Error('trailer not set')
274
+
275
+ const md = QuickFixGraphParser.getMessage(node.element)
276
+ this.definitions.addMessage(md)
277
+ this.containedSets.set(node.id, md)
278
+
279
+ // wrap the message body in StandardHeader + content + StandardTrailer
280
+ this.constructTailNode('StandardHeader', node, this.header.element, NodeElementType.ComponentDeclaration)
281
+ this.expandSet(node)
282
+ this.constructTailNode('StandardTrailer', node, this.trailer.element, NodeElementType.ComponentDeclaration)
283
+ }
284
+
285
+ private componentDefinition (node: GraphNode): void {
286
+ const definition = this.getComponentDefinition(node)
287
+ this.containedSets.set(node.id, definition)
288
+ this.expandSet(node)
289
+ }
290
+
291
+ private groupDefinition (node: GraphNode): void {
292
+ const tail = node.edges[0]?.tail
293
+ if (tail == null) {
294
+ throw new Error(`node ${node} has no edges to find tail for group definition`)
295
+ }
296
+ const definition = this.containedSets.get(tail)
297
+ if (!definition) {
298
+ throw new Error(`node ${node} has no contained set for group definition`)
299
+ }
300
+ this.containedSets.set(node.edges[0].head, definition)
301
+ this.expandSet(node)
302
+ }
303
+
304
+ private inlineGroupDefinition (node: GraphNode): void {
305
+ const edge = node.edges[0]
306
+ if (!edge) {
307
+ throw new Error(`node ${node} has no edges to find tail for inline group definition`)
308
+ }
309
+ const parentSet = this.containedSets.get(edge.tail)
310
+ if (!parentSet) {
311
+ throw new Error(`edge tail ${edge.tail} has no contained set for inline group '${node.name}'`)
312
+ }
313
+ const noOFieldDefinition = this.definitions.simple.get(node.name)
314
+ if (!noOFieldDefinition) {
315
+ throw new Error(`${node.name} does not exist in simple field definitions to construct inline group`)
316
+ }
317
+ const name = node.name
318
+ const definition = new GroupFieldDefinition(name, name, null, noOFieldDefinition, name)
319
+ const containedGroup = new ContainedGroupField(definition, parentSet.fields.length, node.isRequired())
320
+ new ContainedSetBuilder(parentSet).add(containedGroup)
321
+ this.containedSets.set(edge.head, definition)
322
+ this.constructTailNode(name, node, node.element, NodeElementType.GroupDefinition)
323
+ }
324
+
325
+ private simpleFieldDeclaration (node: GraphNode): void {
326
+ const edge = node.edges[0]
327
+ if (!edge) {
328
+ throw new Error(`node ${node} has no edges to find tail for simple field declaration`)
329
+ }
330
+ const parentSet = this.containedSets.get(edge.tail)
331
+ if (!parentSet) {
332
+ throw new Error(`edge tail ${edge.tail} has no contained set for declared field '${node.name}'`)
333
+ }
334
+ const sd = this.definitions.simple.get(node.name)
335
+ if (!sd) {
336
+ throw new Error(`element ${node} cannot be located as a simple field`)
337
+ }
338
+ const containedSimpleField = new ContainedSimpleField(sd, parentSet.fields.length, node.isRequired(), false)
339
+ new ContainedSetBuilder(parentSet).add(containedSimpleField)
340
+ }
341
+
342
+ // ── Set expansion ──
343
+
344
+ /**
345
+ * Walks the children of a definition node and constructs tail nodes for each
346
+ * field/group/component reference, queuing them for resolution.
347
+ */
348
+ private expandSet (node: GraphNode): void {
349
+ for (const child of node.element.elements()) {
350
+ switch (child.name) {
351
+ case 'field': {
352
+ this.expandField(node, child)
353
+ break
354
+ }
355
+ case 'group': {
356
+ this.expandGroup(node, child)
357
+ break
358
+ }
359
+ case 'component': {
360
+ this.expandComponent(node, child)
361
+ break
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ private expandField (node: GraphNode, element: XNode): void {
368
+ this.constructTailNode(QuickFixGraphParser.nameFrom(element), node, element, NodeElementType.SimpleFieldDeclaration)
369
+ }
370
+
371
+ private expandGroup (node: GraphNode, element: XNode): void {
372
+ this.expandSetChild(node, element, NodeElementType.InlineGroupDefinition, NodeElementType.GroupDeclaration)
373
+ }
374
+
375
+ private expandComponent (node: GraphNode, element: XNode): void {
376
+ this.expandSetChild(node, element, NodeElementType.ComponentDefinition, NodeElementType.ComponentDeclaration)
377
+ }
378
+
379
+ private expandSetChild (
380
+ node: GraphNode,
381
+ element: XNode,
382
+ defineElement: NodeElementType,
383
+ declareElement: NodeElementType
384
+ ): void {
385
+ const name = QuickFixGraphParser.nameFrom(element)
386
+ const hasInlinedFields = element.elements().length > 0
387
+ const elementType = hasInlinedFields ? defineElement : declareElement
388
+ this.constructTailNode(name, node, element, elementType)
389
+ }
390
+
391
+ // ── Static helpers ──
392
+
393
+ /**
394
+ * Extract a name from an element's attributes, with the same conventions as the C# parser:
395
+ * strip spaces, prefix with 'F' if the name starts with a digit.
396
+ */
397
+ static nameFrom (element: XNode): string {
398
+ let name = element.attribute('name') ?? ''
399
+ name = name.replace(/ /g, '')
400
+ if (name.length > 0 && name[0] >= '0' && name[0] <= '9') {
401
+ name = `F${name}`
402
+ }
403
+ return name
404
+ }
405
+
406
+ static getField (element: XNode): SimpleFieldDefinition {
407
+ const name = QuickFixGraphParser.nameFrom(element)
408
+ const numberStr = element.attribute('number')
409
+ const tag = numberStr != null ? parseInt(numberStr, 10) : -1
410
+ if (tag < 0) throw new Error(`no tag/number for ${name}`)
411
+ const type = element.attribute('type') ?? 'STRING'
412
+
413
+ const sd = new SimpleFieldDefinition(String(tag), name, name, null, null, type, null)
414
+ // Add enum values
415
+ for (const value of element.elements('value')) {
416
+ const enumKey = value.attribute('enum')
417
+ const description = value.attribute('description') ?? ''
418
+ if (enumKey != null) {
419
+ sd.addEnum(enumKey, description, description)
420
+ }
421
+ }
422
+ return sd
423
+ }
424
+
425
+ static getMessage (element: XNode): MessageDefinition {
426
+ const name = QuickFixGraphParser.nameFrom(element)
427
+ const msgCat = element.attribute('msgcat') ?? ''
428
+ const msgType = element.attribute('msgtype') ?? ''
429
+ return new MessageDefinition(name, name, msgType, msgCat, name)
430
+ }
431
+
432
+ /**
433
+ * Convenience: parse XML text into a fresh FixDefinitions.
434
+ */
435
+ static parse (xml: string, options: QuickFixGraphParserOptions = {}): FixDefinitions {
436
+ // Pre-parse the version so we can construct FixDefinitions correctly
437
+ const doc = SaxTreeBuilder.parse(xml)
438
+ const fixRoot = doc.firstDescendant('fix')
439
+ if (!fixRoot) throw new Error('no <fix> root element')
440
+ const major = parseInt(fixRoot.attribute('major') ?? '0', 10)
441
+ const minor = parseInt(fixRoot.attribute('minor') ?? '0', 10)
442
+ const servicepack = parseInt(fixRoot.attribute('servicepack') ?? '0', 10)
443
+ const description = (major !== 5 || servicepack === 0)
444
+ ? `FIX.${major}.${minor}`
445
+ : `FIX.${major}.${minor}SP${servicepack}`
446
+ const definitions = new FixDefinitions(FixDefinitionSource.QuickFix, VersionUtil.resolve(description))
447
+ const parser = new QuickFixGraphParser(definitions, options)
448
+ return parser.parseDocument(doc)
449
+ }
450
+ }
@@ -0,0 +1,112 @@
1
+ import { XDocument, XElement } from './x-element'
2
+
3
+ /**
4
+ * Builds an in-memory XElement tree from XML using a single SAX pass.
5
+ * After construction, the tree provides DOM-like random access for
6
+ * graph-based parsing and validation.
7
+ */
8
+ export class SaxTreeBuilder {
9
+ /**
10
+ * Parse XML text into an XDocument.
11
+ */
12
+ static parse (xml: string): XDocument {
13
+ const sax = require('sax')
14
+ const parser = sax.parser(true, {})
15
+ const stack: XElement[] = []
16
+ let root: XElement | null = null
17
+ let lastWasSelfClosing = false
18
+
19
+ parser.onopentag = (node: { name: string, attributes: Record<string, string>, isSelfClosing: boolean }) => {
20
+ const element: XElement = {
21
+ name: node.name,
22
+ attributes: { ...node.attributes },
23
+ children: [],
24
+ line: parser.line + 1
25
+ }
26
+
27
+ if (stack.length > 0) {
28
+ stack[stack.length - 1].children.push(element)
29
+ }
30
+
31
+ lastWasSelfClosing = node.isSelfClosing
32
+ if (!node.isSelfClosing) {
33
+ stack.push(element)
34
+ }
35
+ if (!root) root = element
36
+ }
37
+
38
+ parser.onclosetag = () => {
39
+ if (lastWasSelfClosing) {
40
+ lastWasSelfClosing = false
41
+ return
42
+ }
43
+ stack.pop()
44
+ }
45
+
46
+ parser.onerror = (err: Error) => {
47
+ throw new Error(`XML parse error at line ${parser.line + 1}: ${err.message}`)
48
+ }
49
+
50
+ parser.write(xml).close()
51
+
52
+ if (!root) {
53
+ throw new Error('empty XML document')
54
+ }
55
+
56
+ return new XDocument(root)
57
+ }
58
+
59
+ /**
60
+ * Parse XML from a readable stream into an XDocument.
61
+ */
62
+ static async parseStream (readable: NodeJS.ReadableStream): Promise<XDocument> {
63
+ return await new Promise<XDocument>((resolve, reject) => {
64
+ const sax = require('sax')
65
+ const saxStream = sax.createStream(true, {})
66
+ const stack: XElement[] = []
67
+ let root: XElement | null = null
68
+ let lastWasSelfClosing = false
69
+
70
+ saxStream.on('opentag', (node: { name: string, attributes: Record<string, string>, isSelfClosing: boolean }) => {
71
+ const element: XElement = {
72
+ name: node.name,
73
+ attributes: { ...node.attributes },
74
+ children: [],
75
+ line: saxStream._parser.line + 1
76
+ }
77
+
78
+ if (stack.length > 0) {
79
+ stack[stack.length - 1].children.push(element)
80
+ }
81
+
82
+ lastWasSelfClosing = node.isSelfClosing
83
+ if (!node.isSelfClosing) {
84
+ stack.push(element)
85
+ }
86
+ if (!root) root = element
87
+ })
88
+
89
+ saxStream.on('closetag', () => {
90
+ if (lastWasSelfClosing) {
91
+ lastWasSelfClosing = false
92
+ return
93
+ }
94
+ stack.pop()
95
+ })
96
+
97
+ saxStream.on('error', (err: Error) => {
98
+ reject(new Error(`XML parse error: ${err.message}`))
99
+ })
100
+
101
+ saxStream.on('end', () => {
102
+ if (!root) {
103
+ reject(new Error('empty XML document'))
104
+ } else {
105
+ resolve(new XDocument(root))
106
+ }
107
+ })
108
+
109
+ readable.pipe(saxStream)
110
+ })
111
+ }
112
+ }
@@ -0,0 +1,34 @@
1
+ export enum ValidationSeverity {
2
+ Warning = 'Warning',
3
+ Error = 'Error'
4
+ }
5
+
6
+ export interface ValidationError {
7
+ readonly severity: ValidationSeverity
8
+ readonly code: string
9
+ readonly message: string
10
+ readonly elementName?: string
11
+ readonly elementType?: string
12
+ readonly lineNumber?: number
13
+ readonly suggestion?: string
14
+ }
15
+
16
+ export class DictionaryValidationException extends Error {
17
+ constructor (public readonly errors: ReadonlyArray<ValidationError>) {
18
+ const errorCount = errors.filter(e => e.severity === ValidationSeverity.Error).length
19
+ const warningCount = errors.filter(e => e.severity === ValidationSeverity.Warning).length
20
+ const header = `FIX dictionary validation failed with ${errorCount} error(s) and ${warningCount} warning(s):`
21
+ const details = errors
22
+ .filter(e => e.severity === ValidationSeverity.Error)
23
+ .slice(0, 20)
24
+ .map(e => {
25
+ let line = ` [${e.code}] ${e.message}`
26
+ if (e.lineNumber != null) line += ` (line ${e.lineNumber})`
27
+ if (e.suggestion) line += ` — ${e.suggestion}`
28
+ return line
29
+ })
30
+ .join('\n')
31
+ super(`${header}\n${details}`)
32
+ this.name = 'DictionaryValidationException'
33
+ }
34
+ }