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.
- package/BACKPORT_PLAN.md +120 -10
- package/DEMO_PORT_PLAN.md +286 -0
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +39 -0
- package/dist/dictionary/parser/quickfix/dictionary-validator.js +321 -0
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -0
- package/dist/dictionary/parser/quickfix/index-visitor.d.ts +10 -0
- package/dist/dictionary/parser/quickfix/index-visitor.js +68 -0
- package/dist/dictionary/parser/quickfix/index-visitor.js.map +1 -0
- package/dist/dictionary/parser/quickfix/index.d.ts +7 -0
- package/dist/dictionary/parser/quickfix/index.js +7 -0
- package/dist/dictionary/parser/quickfix/index.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.d.ts +13 -0
- package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js +65 -0
- package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js.map +1 -0
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +73 -0
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +363 -0
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -0
- package/dist/dictionary/parser/quickfix/sax-tree-builder.d.ts +5 -0
- package/dist/dictionary/parser/quickfix/sax-tree-builder.js +103 -0
- package/dist/dictionary/parser/quickfix/sax-tree-builder.js.map +1 -0
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +17 -0
- package/dist/dictionary/parser/quickfix/validation-error.js +32 -0
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -0
- package/dist/dictionary/parser/quickfix/x-element.d.ts +26 -0
- package/dist/dictionary/parser/quickfix/x-element.js +82 -0
- package/dist/dictionary/parser/quickfix/x-element.js.map +1 -0
- package/dist/store/fix-msg-ascii-store-resend.js +6 -0
- package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
- package/dist/store/store-config.d.ts +1 -0
- package/dist/store/store-config.js.map +1 -1
- package/dist/transport/ascii/ascii-session.js +3 -0
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/util/definition-factory.js +1 -1
- package/dist/util/definition-factory.js.map +1 -1
- package/jsfix.test_client.txt +67 -67
- package/jsfix.test_server.txt +64 -64
- package/package.json +6 -6
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +473 -0
- package/src/dictionary/parser/quickfix/index-visitor.ts +100 -0
- package/src/dictionary/parser/quickfix/index.ts +7 -0
- package/src/dictionary/parser/quickfix/quick-fix-graph-file-parser.ts +63 -0
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +450 -0
- package/src/dictionary/parser/quickfix/sax-tree-builder.ts +112 -0
- package/src/dictionary/parser/quickfix/validation-error.ts +34 -0
- package/src/dictionary/parser/quickfix/x-element.ts +115 -0
- package/src/store/fix-msg-ascii-store-resend.ts +8 -0
- package/src/store/store-config.ts +1 -0
- package/src/transport/ascii/ascii-session.ts +4 -0
- 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
|
+
}
|