jspurefix 5.2.0 → 5.5.0
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 +135 -39
- package/dist/config/js-fix-config.d.ts +2 -0
- package/dist/config/js-fix-config.js.map +1 -1
- 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/file-session-store.d.ts +42 -0
- package/dist/store/file-session-store.js +256 -0
- package/dist/store/file-session-store.js.map +1 -0
- package/dist/store/file-session-stream-provider.d.ts +25 -0
- package/dist/store/file-session-stream-provider.js +162 -0
- package/dist/store/file-session-stream-provider.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/fix-session-store-factory.d.ts +13 -0
- package/dist/store/fix-session-store-factory.js +21 -0
- package/dist/store/fix-session-store-factory.js.map +1 -0
- package/dist/store/fix-session-store.d.ts +19 -0
- package/dist/store/fix-session-store.js +3 -0
- package/dist/store/fix-session-store.js.map +1 -0
- package/dist/store/index.d.ts +9 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/memory-session-store.d.ts +27 -0
- package/dist/store/memory-session-store.js +104 -0
- package/dist/store/memory-session-store.js.map +1 -0
- package/dist/store/memory-session-stream-provider.d.ts +26 -0
- package/dist/store/memory-session-stream-provider.js +103 -0
- package/dist/store/memory-session-stream-provider.js.map +1 -0
- package/dist/store/session-id.d.ts +9 -0
- package/dist/store/session-id.js +55 -0
- package/dist/store/session-id.js.map +1 -0
- package/dist/store/session-stream-provider.d.ts +15 -0
- package/dist/store/session-stream-provider.js +3 -0
- package/dist/store/session-stream-provider.js.map +1 -0
- package/dist/store/store-config.d.ts +5 -0
- package/dist/store/store-config.js +3 -0
- package/dist/store/store-config.js.map +1 -0
- package/dist/transport/ascii/ascii-session.d.ts +6 -1
- package/dist/transport/ascii/ascii-session.js +37 -5
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/session/session-description.d.ts +2 -0
- package/dist/transport/session/session-description.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/config/js-fix-config.ts +2 -0
- 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/file-session-store.ts +294 -0
- package/src/store/file-session-stream-provider.ts +123 -0
- package/src/store/fix-msg-ascii-store-resend.ts +8 -0
- package/src/store/fix-session-store-factory.ts +31 -0
- package/src/store/fix-session-store.ts +37 -0
- package/src/store/index.ts +9 -0
- package/src/store/memory-session-store.ts +102 -0
- package/src/store/memory-session-stream-provider.ts +97 -0
- package/src/store/session-id.ts +32 -0
- package/src/store/session-stream-provider.ts +74 -0
- package/src/store/store-config.ts +16 -0
- package/src/transport/ascii/ascii-session.ts +57 -6
- package/src/transport/session/session-description.ts +2 -0
- package/src/util/definition-factory.ts +2 -2
package/BACKPORT_PLAN.md
CHANGED
|
@@ -44,22 +44,9 @@ Instrument tags (15, 22, 48, 55) are scattered among order tags (21, 38, 40, 44,
|
|
|
44
44
|
2. **Fragment Detection**: If a tag belonging to an already-exited depth-1 component is encountered, that component is added to `FragmentedComponents`.
|
|
45
45
|
3. **Optimised Access**: Only fragmented components get expensive `SegmentView` construction via `TagIndex`. Non-fragmented components use simple position ranges (no overhead).
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Status: **DONE**
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|------|--------|
|
|
51
|
-
| `src/buffer/ascii/ascii-segment-parser.ts` | Add fragment detection (ExitedDepth1Components, FragmentedComponents tracking) |
|
|
52
|
-
| `src/buffer/segment/segment-description.ts` | May need SegmentView reference for fragmented components |
|
|
53
|
-
| New: `src/buffer/segment/segment-view.ts` | Port SegmentView for non-contiguous tag access |
|
|
54
|
-
| New: `src/buffer/ascii/tag-index.ts` | Port TagIndex for tag span indexing |
|
|
55
|
-
|
|
56
|
-
### Test Data
|
|
57
|
-
|
|
58
|
-
Port from `~/dev/cs/cspurefix/PureFix.Test.ModularTypes/Data/examples/FIX.4.4/fixsim-examples.txt` — 46 real FIX messages including fragmented Instrument components. Write a failing test first to prove the problem, then fix.
|
|
59
|
-
|
|
60
|
-
### C# Test Reference
|
|
61
|
-
|
|
62
|
-
`~/dev/cs/cspurefix/PureFix.Test.ModularTypes/FixSimTest.cs` — parses all 46 messages, verifies type counts, deserialises ExecutionReport to strongly-typed object.
|
|
49
|
+
Completed in PR #110 (commits `17bcf9b`, `9223192`). Tag index, fragment detection, and segment view ported. Also includes `d660460` (relax body length constraint) and `4197ce0` (relax raw data length constraint for replayed messages).
|
|
63
50
|
|
|
64
51
|
---
|
|
65
52
|
|
|
@@ -80,12 +67,9 @@ m_parser.Reset();
|
|
|
80
67
|
|
|
81
68
|
Also resets transient coordinator state (logon retry count, timeout recovery attempts) while preserving sequence numbers.
|
|
82
69
|
|
|
83
|
-
###
|
|
70
|
+
### Status: **DONE**
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|------|--------|
|
|
87
|
-
| `src/transport/session/fix-session.ts` | Add parser reset in reconnection path |
|
|
88
|
-
| `src/buffer/ascii/ascii-parser.ts` | Ensure `reset()` method clears all partial state |
|
|
72
|
+
Completed in PR #111 (commit `deaccc3`). Parser state reset on disconnect to prevent buffer corruption.
|
|
89
73
|
|
|
90
74
|
---
|
|
91
75
|
|
|
@@ -220,22 +204,20 @@ Depends on 4A + 4B.
|
|
|
220
204
|
|
|
221
205
|
#### PR 5A: Storm protection wiring (low risk)
|
|
222
206
|
|
|
223
|
-
|
|
224
|
-
|------|--------|
|
|
225
|
-
| `src/transport/ascii/ascii-session.ts` | Use `ResendAction` from coordinator to decide between sending ResendRequest, waiting, or gap-filling |
|
|
226
|
-
| New: `src/test/session/resend-storm-protection.test.ts` | ~5 tests |
|
|
207
|
+
### Status: **DONE**
|
|
227
208
|
|
|
228
|
-
|
|
209
|
+
Storm protection was largely wired during PR 3C/3D. Remaining gaps completed:
|
|
210
|
+
- Accept gap-triggering message instead of dropping it (match C# behaviour)
|
|
211
|
+
- Add pending gap range check for delayed messages with `seqDelta <= 0`
|
|
212
|
+
- Add `coordinator.tick()` to session tick loop for resend request timeout cleanup
|
|
229
213
|
|
|
230
214
|
#### PR 5B: ResendGapFillOnly mode (zero risk, independent)
|
|
231
215
|
|
|
232
216
|
| File | Action |
|
|
233
217
|
|------|--------|
|
|
234
|
-
|
|
235
|
-
| `src/store/fix-msg-ascii-store-resend.ts` | Early return path when enabled — always GapFill instead of replaying |
|
|
236
|
-
| New: `src/test/store/resend-gap-fill-only.test.ts` | ~3 tests |
|
|
218
|
+
### Status: **DONE**
|
|
237
219
|
|
|
238
|
-
|
|
220
|
+
Added `resendGapFillOnly` option to `StoreConfig`. When enabled, `FixMsgAsciiStoreResend` always returns a single GapFill instead of replaying stored messages — prevents accidental duplicate order execution for client/initiator sessions. 5 tests added.
|
|
239
221
|
|
|
240
222
|
---
|
|
241
223
|
|
|
@@ -257,24 +239,138 @@ PR 5B (ResendGapFillOnly) ──── independent, can be done anytime
|
|
|
257
239
|
|
|
258
240
|
| PR | Risk | Reason |
|
|
259
241
|
|----|------|--------|
|
|
242
|
+
| 1 | Medium | Non-contiguous tag parsing — **DONE** (PR #110) |
|
|
243
|
+
| 2 | Low | Parser reset on disconnect — **DONE** (PR #111) |
|
|
260
244
|
| 3A, 3B | None | New files only — **DONE** |
|
|
261
245
|
| 3C | HIGH | Refactors `checkSeqNo` — pure refactor, same behaviour, but core message path — **DONE** |
|
|
262
246
|
| 3D | Medium | Adds new capabilities (logon retry, PossDupFlag, ResetSeqNum, timeout recovery) — **DONE** |
|
|
263
|
-
| 4A, 4B | None | New files only |
|
|
264
|
-
| 4C | Low | New file, tested with mocks |
|
|
265
|
-
| 4D | Medium | Changes send path, store errors must not block sends |
|
|
266
|
-
| 5A | Low | Wiring only, coordinator makes decisions |
|
|
267
|
-
| 5B | None | Additive config option |
|
|
247
|
+
| 4A, 4B | None | New files only — **DONE** |
|
|
248
|
+
| 4C | Low | New file, tested with mocks — **DONE** (PR #120) |
|
|
249
|
+
| 4D | Medium | Changes send path, store errors must not block sends — **DONE** |
|
|
250
|
+
| 5A | Low | Wiring only, coordinator makes decisions — **DONE** |
|
|
251
|
+
| 5B | None | Additive config option — **DONE** |
|
|
268
252
|
|
|
269
253
|
---
|
|
270
254
|
|
|
271
|
-
## Phase 6:
|
|
255
|
+
## Phase 6: QuickFix XML Parser Rework
|
|
256
|
+
|
|
257
|
+
**Priority: Medium | Risk: HIGH | Scope: Large (multi-session)**
|
|
258
|
+
|
|
259
|
+
### Background
|
|
260
|
+
|
|
261
|
+
The C# QuickFix XML parser (`QuickFixXmlFileParser.cs`) is architecturally superior:
|
|
262
|
+
- **DOM-based**: parses XML once into `XDocument`, then walks the tree
|
|
263
|
+
- **Graph-based resolution**: nodes + edges + work queue, deterministic single logical pass
|
|
264
|
+
- **Post-processor**: `IndexVisitor` + `ContainedFieldCollector` with memoisation ensures every `ContainedFieldSet` knows all tags below it
|
|
265
|
+
- **Pre-parse validation**: `DictionaryValidator` catches missing fields, duplicates, undefined references with Levenshtein typo suggestions
|
|
266
|
+
|
|
267
|
+
The TS parser uses SAX streaming with iterative N-pass (up to 5x) forward reference resolution. This works but is fragile, hard to reason about, and diverges from C#.
|
|
268
|
+
|
|
269
|
+
**Scope**: Only the QuickFix parser chain needs rework. FIXML (XSD, already has include graph) and Repository (sequential file parsing) are fine as-is.
|
|
270
|
+
|
|
271
|
+
### Strategy: SAX → In-Memory Tree → Graph Resolution
|
|
272
|
+
|
|
273
|
+
Rather than replacing SAX with a new XML library, wrap SAX to build a lightweight in-memory element tree on a single pass. This tree then provides DOM-like random access for the graph resolver, without introducing new dependencies.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
interface XElement {
|
|
277
|
+
name: string
|
|
278
|
+
attributes: Record<string, string>
|
|
279
|
+
children: XElement[]
|
|
280
|
+
line?: number
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Delivery: 6 PRs
|
|
285
|
+
|
|
286
|
+
#### PR 6A: XElement tree builder (new files only, zero risk)
|
|
287
|
+
|
|
288
|
+
### Status: **DONE** (PR #124)
|
|
289
|
+
|
|
290
|
+
`XElement` interface + `XDocument`/`XNode` query wrappers + `SaxTreeBuilder` (single SAX pass → in-memory tree). 51 tests across all FIX dictionaries.
|
|
291
|
+
|
|
292
|
+
#### PR 6B: DictionaryValidator (new files only, zero risk)
|
|
293
|
+
|
|
294
|
+
### Status: **DONE**
|
|
295
|
+
|
|
296
|
+
Three-pass validator ported from C#: collect definitions, validate references (with Levenshtein "did you mean" suggestions), check unused definitions. 40 tests including validation against all real FIX dictionaries.
|
|
297
|
+
|
|
298
|
+
#### PR 6C: Graph-based parser + IndexVisitor (medium risk)
|
|
299
|
+
|
|
300
|
+
### Status: **DONE**
|
|
301
|
+
|
|
302
|
+
Verbatim port of C# `QuickFixXmlFileParser`: `GraphNode`/`Edge`/`NodeElementType` + work queue + field/component/group/message resolution. New parser sits alongside legacy parser for safe comparison testing.
|
|
303
|
+
|
|
304
|
+
Includes `IndexVisitor` post-processor (originally PR 6D, merged into 6C because the parser doesn't work without it). Walks every message in post-order, clears aggregated tag indices on each set, and re-adds direct fields via `ContainedSetBuilder` so parents correctly know all descendant tags.
|
|
305
|
+
|
|
306
|
+
**Comparison test results:** Graph parser produces a **superset** of legacy parser output for FIX50SP2 — correctly resolves deeply nested forward references (e.g., DividendFXTriggerDateBusinessCenter chain) that the legacy 5-pass iterative parser truncates. This is a correctness improvement.
|
|
307
|
+
|
|
308
|
+
41 new tests including comparison against legacy parser for FIX 4.2, 4.3, 4.4, and 5.0SP2.
|
|
309
|
+
|
|
310
|
+
#### PR 6E: Switch default parser (HIGH risk)
|
|
311
|
+
|
|
312
|
+
### Status: **DONE**
|
|
313
|
+
|
|
314
|
+
Added `QuickFixGraphFileParser` adapter that extends `FixParser` and matches the legacy parser's `(MakeDuplex, GetJsFixLogger)` constructor signature. `DefinitionFactory.getParser()` now instantiates the graph parser for QuickFix XML — all dictionary loading goes through the graph parser by default.
|
|
315
|
+
|
|
316
|
+
The legacy `QuickFixXmlFileParser` remains exported for backward compatibility (anyone importing it directly still gets the old behaviour), but no production code in the project uses it.
|
|
317
|
+
|
|
318
|
+
**Test fallout fixed:**
|
|
319
|
+
- `src/test/env/data/fix5-mod.xml` had a redundant 215-line "admin fields" section that duplicated 62 fields already present in the canonical fields section. Plus the `HopGrp` and `MsgTypeGrp` components were defined twice. The legacy parser silently swallowed both classes of duplicate; the graph parser's validator correctly flags them as errors. Removed the redundant admin section (kept the unique `OTP` custom tag) and the duplicate component definitions.
|
|
320
|
+
- `src/test/ascii/qf-50sp2-dict.test.ts` had two assertions where `Instrument` and `TrdCapRptSideGrp` were expected as `required=false` for TradeCaptureReport. The dictionary clearly says `required='Y'` for both. The legacy parser was producing `required=false` (a real bug); the graph parser correctly returns `true`. Updated the assertions.
|
|
321
|
+
- Updated `getTrimDefinitions` in the same file to use `QuickFixGraphFileParser` instead of the legacy parser for consistency.
|
|
322
|
+
|
|
323
|
+
All 533 tests pass with the graph parser as the default.
|
|
324
|
+
|
|
325
|
+
Note: `parse-progress.ts`, `parse-state.ts`, and the legacy parser itself are NOT yet deleted — that's deferred to a follow-up cleanup PR after a release cycle to give downstream consumers time to migrate.
|
|
326
|
+
|
|
327
|
+
#### PR 6F: Fix Trim function (medium risk)
|
|
328
|
+
|
|
329
|
+
### Status: **DONE — TS trim was already correct**
|
|
330
|
+
|
|
331
|
+
After investigation: the TS trim function (`QuickFixXmlFileBuilder`) is correct. The "bug" we suspected was actually introduced during the C# port — see C# commit `2ee4620c`. The original C# `WriteComponents` used a `foreach` over a snapshot of `_seenComponents`, dropping any components discovered during iteration. The C# fix added `ProcessComponentFields` for eager recursive collection.
|
|
332
|
+
|
|
333
|
+
The TS version uses `while (components.length > 0) { components.pop() }` — a proper iterative discovery loop where newly-pushed components are processed in subsequent iterations. This pattern doesn't have the bug.
|
|
334
|
+
|
|
335
|
+
**Added** comprehensive round-trip tests in `src/test/dictionary/trim-round-trip.test.ts`:
|
|
336
|
+
- Trim → reparse with strict graph parser validation
|
|
337
|
+
- Compare flattened tag sets at the message level
|
|
338
|
+
- Deep nested-set comparison (every component/group in every message)
|
|
339
|
+
- Worst case: trim ALL 116 FIX50SP2 messages and deep-compare every one
|
|
340
|
+
|
|
341
|
+
All 9 tests pass. No code changes needed to `quick-fix-xml-file-builder.ts`.
|
|
342
|
+
|
|
343
|
+
### Dependency Graph
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
PR 6A (XElement tree) ──────────┐
|
|
347
|
+
├──→ PR 6C (Graph parser) ──→ PR 6E (Switch default)
|
|
348
|
+
PR 6B (DictionaryValidator) ───┘ │
|
|
349
|
+
↓
|
|
350
|
+
PR 6D (IndexVisitor) ──→ PR 6E
|
|
351
|
+
|
|
352
|
+
PR 6F (Fix Trim) ──── after 6C is stable
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Risk Summary
|
|
356
|
+
|
|
357
|
+
| PR | Risk | Reason |
|
|
358
|
+
|----|------|--------|
|
|
359
|
+
| 6A | None | New files only, SAX wrapper — **DONE** (PR #124) |
|
|
360
|
+
| 6B | None | New files only, validation — **DONE** |
|
|
361
|
+
| 6C | Medium | Graph parser + IndexVisitor — sits alongside legacy parser — **DONE** |
|
|
362
|
+
| 6D | (merged into 6C) | IndexVisitor was needed for 6C to work correctly |
|
|
363
|
+
| 6E | HIGH | Switches default parser — exposed 2 legacy parser bugs (now fixed) — **DONE** |
|
|
364
|
+
| 6F | None | TS trim already correct — round-trip tests added — **DONE** |
|
|
365
|
+
|
|
366
|
+
### Test Strategy
|
|
272
367
|
|
|
273
|
-
|
|
368
|
+
The parser is the foundation of the entire system. Test strategy must be comprehensive:
|
|
274
369
|
|
|
275
|
-
|
|
276
|
-
-
|
|
277
|
-
-
|
|
370
|
+
1. **Comparison tests**: parse every FIX XML dictionary (4.2, 4.3, 4.4, 5.0SP2) with both old and new parser, diff the resulting `FixDefinitions`
|
|
371
|
+
2. **Round-trip tests**: parse → trim → reparse, verify definitions match
|
|
372
|
+
3. **Micro-dictionary tests** (future hardening): use Trim to create single-message dictionaries, then mutate them (remove fields, duplicate tags, etc.) to test validator edge cases
|
|
373
|
+
4. **Regression anchor**: snapshot the `FixDefinitions` output for each FIX version before any changes — tests assert against snapshots
|
|
278
374
|
|
|
279
375
|
---
|
|
280
376
|
|
|
@@ -3,6 +3,7 @@ import { ISessionDescription } from '../transport/session/session-description';
|
|
|
3
3
|
import { ISessionMsgFactory } from '../transport/session/session-msg-factory';
|
|
4
4
|
import { JsFixLoggerFactory } from './js-fix-logger-factory';
|
|
5
5
|
import { DependencyContainer } from 'tsyringe';
|
|
6
|
+
import { IFixSessionStoreFactory } from '../store/fix-session-store-factory';
|
|
6
7
|
export interface IJsFixConfig {
|
|
7
8
|
factory: ISessionMsgFactory | null;
|
|
8
9
|
definitions: FixDefinitions;
|
|
@@ -11,6 +12,7 @@ export interface IJsFixConfig {
|
|
|
11
12
|
logDelimiter?: number;
|
|
12
13
|
logFactory: JsFixLoggerFactory;
|
|
13
14
|
sessionContainer: DependencyContainer;
|
|
15
|
+
sessionStoreFactory?: IFixSessionStoreFactory;
|
|
14
16
|
}
|
|
15
17
|
export declare class JsFixConfig implements IJsFixConfig {
|
|
16
18
|
readonly factory: ISessionMsgFactory | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"js-fix-config.js","sourceRoot":"","sources":["../../src/config/js-fix-config.ts"],"names":[],"mappings":";;;AAIA,2DAAqD;AACrD,6DAAwD;
|
|
1
|
+
{"version":3,"file":"js-fix-config.js","sourceRoot":"","sources":["../../src/config/js-fix-config.ts"],"names":[],"mappings":";;;AAIA,2DAAqD;AACrD,6DAAwD;AAexD,MAAa,WAAW;IAGtB,YACkB,OAAkC,EAClC,WAA2B,EAC3B,WAAgC,EAChC,YAAoB,wBAAU,CAAC,GAAG,EAClC,aAAiC,IAAI,mCAAe,EAAE;QAJtD,YAAO,GAAP,OAAO,CAA2B;QAClC,gBAAW,GAAX,WAAW,CAAgB;QAC3B,gBAAW,GAAX,WAAW,CAAqB;QAChC,cAAS,GAAT,SAAS,CAAyB;QAClC,eAAU,GAAV,UAAU,CAA4C;QAPjE,iBAAY,GAAW,wBAAU,CAAC,IAAI,CAAA;IAQ7C,CAAC;CACF;AAVD,kCAUC","sourcesContent":["import { FixDefinitions } from '../dictionary/definition'\nimport { ISessionDescription } from '../transport/session/session-description'\nimport { ISessionMsgFactory } from '../transport/session/session-msg-factory'\nimport { JsFixLoggerFactory } from './js-fix-logger-factory'\nimport { EmptyLogFactory } from './empty-log-factory'\nimport { AsciiChars } from '../buffer/ascii/ascii-chars'\nimport { DependencyContainer } from 'tsyringe'\nimport { IFixSessionStoreFactory } from '../store/fix-session-store-factory'\n\nexport interface IJsFixConfig {\n factory: ISessionMsgFactory | null\n definitions: FixDefinitions\n description: ISessionDescription\n delimiter?: number\n logDelimiter?: number\n logFactory: JsFixLoggerFactory\n sessionContainer: DependencyContainer\n sessionStoreFactory?: IFixSessionStoreFactory\n}\n\nexport class JsFixConfig implements IJsFixConfig {\n public logDelimiter: number = AsciiChars.Pipe\n public sessionContainer: DependencyContainer\n constructor (\n public readonly factory: ISessionMsgFactory | null,\n public readonly definitions: FixDefinitions,\n public readonly description: ISessionDescription,\n public readonly delimiter: number = AsciiChars.Soh,\n public readonly logFactory: JsFixLoggerFactory = new EmptyLogFactory()) {\n }\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { XDocument } from './x-element';
|
|
2
|
+
import { ValidationError } from './validation-error';
|
|
3
|
+
export declare class DictionaryValidator {
|
|
4
|
+
private readonly _errors;
|
|
5
|
+
private readonly fieldsByName;
|
|
6
|
+
private readonly fieldsByTag;
|
|
7
|
+
private readonly componentsByName;
|
|
8
|
+
private readonly messagesByName;
|
|
9
|
+
private readonly messagesByMsgType;
|
|
10
|
+
private readonly fieldNamesCaseInsensitive;
|
|
11
|
+
private readonly componentNamesCaseInsensitive;
|
|
12
|
+
private readonly referencedFields;
|
|
13
|
+
private readonly referencedComponents;
|
|
14
|
+
private readonly allFieldNames;
|
|
15
|
+
private readonly allComponentNames;
|
|
16
|
+
get errors(): ReadonlyArray<ValidationError>;
|
|
17
|
+
get hasErrors(): boolean;
|
|
18
|
+
get hasWarnings(): boolean;
|
|
19
|
+
validate(doc: XDocument): void;
|
|
20
|
+
throwIfErrors(): void;
|
|
21
|
+
private collectFieldDefinitions;
|
|
22
|
+
private validateFieldDefinition;
|
|
23
|
+
private validateFieldEnums;
|
|
24
|
+
private collectComponentDefinitions;
|
|
25
|
+
private validateComponentDefinition;
|
|
26
|
+
private collectMessageDefinitions;
|
|
27
|
+
private validateMessageDefinition;
|
|
28
|
+
private validateHeader;
|
|
29
|
+
private validateTrailer;
|
|
30
|
+
private validateComponentReferences;
|
|
31
|
+
private validateMessageReferences;
|
|
32
|
+
private validateFieldReferences;
|
|
33
|
+
private validateComponentReference;
|
|
34
|
+
private checkUnusedDefinitions;
|
|
35
|
+
private addError;
|
|
36
|
+
private addWarning;
|
|
37
|
+
static findSimilar(input: string, candidates: string[]): string | null;
|
|
38
|
+
static levenshteinDistance(s1: string, s2: string): number;
|
|
39
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DictionaryValidator = void 0;
|
|
4
|
+
const validation_error_1 = require("./validation-error");
|
|
5
|
+
class DictionaryValidator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._errors = [];
|
|
8
|
+
this.fieldsByName = new Map();
|
|
9
|
+
this.fieldsByTag = new Map();
|
|
10
|
+
this.componentsByName = new Map();
|
|
11
|
+
this.messagesByName = new Map();
|
|
12
|
+
this.messagesByMsgType = new Map();
|
|
13
|
+
this.fieldNamesCaseInsensitive = new Map();
|
|
14
|
+
this.componentNamesCaseInsensitive = new Map();
|
|
15
|
+
this.referencedFields = new Set();
|
|
16
|
+
this.referencedComponents = new Set();
|
|
17
|
+
this.allFieldNames = [];
|
|
18
|
+
this.allComponentNames = [];
|
|
19
|
+
}
|
|
20
|
+
get errors() {
|
|
21
|
+
return this._errors;
|
|
22
|
+
}
|
|
23
|
+
get hasErrors() {
|
|
24
|
+
return this._errors.some(e => e.severity === validation_error_1.ValidationSeverity.Error);
|
|
25
|
+
}
|
|
26
|
+
get hasWarnings() {
|
|
27
|
+
return this._errors.some(e => e.severity === validation_error_1.ValidationSeverity.Warning);
|
|
28
|
+
}
|
|
29
|
+
validate(doc) {
|
|
30
|
+
this.collectFieldDefinitions(doc);
|
|
31
|
+
this.collectComponentDefinitions(doc);
|
|
32
|
+
this.collectMessageDefinitions(doc);
|
|
33
|
+
this.validateHeader(doc);
|
|
34
|
+
this.validateTrailer(doc);
|
|
35
|
+
this.validateComponentReferences(doc);
|
|
36
|
+
this.validateMessageReferences(doc);
|
|
37
|
+
this.checkUnusedDefinitions();
|
|
38
|
+
}
|
|
39
|
+
throwIfErrors() {
|
|
40
|
+
if (this.hasErrors) {
|
|
41
|
+
throw new validation_error_1.DictionaryValidationException(this._errors);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
collectFieldDefinitions(doc) {
|
|
45
|
+
const fieldsNode = doc.firstDescendant('fields');
|
|
46
|
+
if (!fieldsNode) {
|
|
47
|
+
this.addError('MISSING_FIELDS', 'No <fields> section found in dictionary');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const field of fieldsNode.elements('field')) {
|
|
51
|
+
this.validateFieldDefinition(field);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
validateFieldDefinition(field) {
|
|
55
|
+
const name = field.attribute('name');
|
|
56
|
+
const numberStr = field.attribute('number');
|
|
57
|
+
const type = field.attribute('type');
|
|
58
|
+
const lineNumber = field.line;
|
|
59
|
+
if (!name) {
|
|
60
|
+
this.addError('FIELD_NO_NAME', 'Field definition missing \'name\' attribute', undefined, undefined, lineNumber);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const tag = numberStr != null ? parseInt(numberStr, 10) : NaN;
|
|
64
|
+
if (!numberStr || isNaN(tag)) {
|
|
65
|
+
this.addError('FIELD_NO_TAG', `Field '${name}' missing or invalid 'number' attribute`, name, 'field', lineNumber);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!type) {
|
|
69
|
+
this.addWarning('FIELD_NO_TYPE', `Field '${name}' missing 'type' attribute, defaulting to STRING`, name, 'field', lineNumber);
|
|
70
|
+
}
|
|
71
|
+
this.allFieldNames.push(name);
|
|
72
|
+
const existingByName = this.fieldsByName.get(name);
|
|
73
|
+
if (existingByName) {
|
|
74
|
+
this.addError('DUPLICATE_FIELD_NAME', `Duplicate field name '${name}' (tag ${tag}). Previously defined with tag ${existingByName.tag}`, name, 'field', lineNumber, existingByName.tag === tag ? 'Remove the duplicate definition' : 'Use unique field names');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const existingByTag = this.fieldsByTag.get(tag);
|
|
78
|
+
if (existingByTag) {
|
|
79
|
+
this.addError('DUPLICATE_FIELD_TAG', `Duplicate field tag ${tag} for '${name}'. Tag already used by field '${existingByTag.name}'`, name, 'field', lineNumber, 'Each tag number must be unique');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const info = { name, tag, type: type !== null && type !== void 0 ? type : 'STRING', lineNumber };
|
|
83
|
+
this.fieldsByName.set(name, info);
|
|
84
|
+
this.fieldsByTag.set(tag, info);
|
|
85
|
+
this.fieldNamesCaseInsensitive.set(name.toLowerCase(), name);
|
|
86
|
+
this.validateFieldEnums(field, name, lineNumber);
|
|
87
|
+
}
|
|
88
|
+
validateFieldEnums(field, fieldName, lineNumber) {
|
|
89
|
+
var _a;
|
|
90
|
+
const values = field.elements('value');
|
|
91
|
+
if (values.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const seenEnumKeys = new Set();
|
|
94
|
+
const seenEnumDescriptions = new Set();
|
|
95
|
+
for (const value of values) {
|
|
96
|
+
const enumKey = value.attribute('enum');
|
|
97
|
+
const description = value.attribute('description');
|
|
98
|
+
const valueLine = (_a = value.line) !== null && _a !== void 0 ? _a : lineNumber;
|
|
99
|
+
if (!enumKey) {
|
|
100
|
+
this.addError('ENUM_NO_KEY', `Field '${fieldName}' has enum value without 'enum' attribute`, fieldName, 'field', valueLine);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!description) {
|
|
104
|
+
this.addWarning('ENUM_NO_DESC', `Field '${fieldName}' enum '${enumKey}' has no description`, fieldName, 'field', valueLine);
|
|
105
|
+
}
|
|
106
|
+
if (seenEnumKeys.has(enumKey)) {
|
|
107
|
+
this.addError('DUPLICATE_ENUM_KEY', `Field '${fieldName}' has duplicate enum key '${enumKey}'`, fieldName, 'field', valueLine);
|
|
108
|
+
}
|
|
109
|
+
seenEnumKeys.add(enumKey);
|
|
110
|
+
if (description != null) {
|
|
111
|
+
const descLower = description.toLowerCase();
|
|
112
|
+
if (seenEnumDescriptions.has(descLower)) {
|
|
113
|
+
this.addWarning('DUPLICATE_ENUM_DESC', `Field '${fieldName}' has duplicate enum description '${description}' which may cause naming conflicts`, fieldName, 'field', valueLine);
|
|
114
|
+
}
|
|
115
|
+
seenEnumDescriptions.add(descLower);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
collectComponentDefinitions(doc) {
|
|
120
|
+
const componentsNode = doc.firstDescendant('components');
|
|
121
|
+
if (!componentsNode) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const component of componentsNode.elements('component')) {
|
|
125
|
+
this.validateComponentDefinition(component);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
validateComponentDefinition(component) {
|
|
129
|
+
var _a;
|
|
130
|
+
const name = component.attribute('name');
|
|
131
|
+
const lineNumber = component.line;
|
|
132
|
+
if (!name) {
|
|
133
|
+
this.addError('COMPONENT_NO_NAME', 'Component definition missing \'name\' attribute', undefined, undefined, lineNumber);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.allComponentNames.push(name);
|
|
137
|
+
const existing = this.componentsByName.get(name);
|
|
138
|
+
if (existing) {
|
|
139
|
+
this.addError('DUPLICATE_COMPONENT', `Duplicate component name '${name}'`, name, 'component', lineNumber, `Previously defined at line ${(_a = existing.lineNumber) !== null && _a !== void 0 ? _a : '?'}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.componentsByName.set(name, { name, lineNumber });
|
|
143
|
+
this.componentNamesCaseInsensitive.set(name.toLowerCase(), name);
|
|
144
|
+
this.validateFieldReferences(component, name, 'component');
|
|
145
|
+
}
|
|
146
|
+
collectMessageDefinitions(doc) {
|
|
147
|
+
const messagesNode = doc.firstDescendant('messages');
|
|
148
|
+
if (!messagesNode) {
|
|
149
|
+
this.addError('MISSING_MESSAGES', 'No <messages> section found in dictionary');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const message of messagesNode.elements('message')) {
|
|
153
|
+
this.validateMessageDefinition(message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
validateMessageDefinition(message) {
|
|
157
|
+
var _a;
|
|
158
|
+
const name = message.attribute('name');
|
|
159
|
+
const msgType = message.attribute('msgtype');
|
|
160
|
+
const lineNumber = message.line;
|
|
161
|
+
if (!name) {
|
|
162
|
+
this.addError('MESSAGE_NO_NAME', 'Message definition missing \'name\' attribute', undefined, undefined, lineNumber);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!msgType) {
|
|
166
|
+
this.addError('MESSAGE_NO_MSGTYPE', `Message '${name}' missing 'msgtype' attribute`, name, 'message', lineNumber);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const existingByName = this.messagesByName.get(name);
|
|
170
|
+
if (existingByName) {
|
|
171
|
+
this.addError('DUPLICATE_MESSAGE_NAME', `Duplicate message name '${name}'`, name, 'message', lineNumber, `Previously defined at line ${(_a = existingByName.lineNumber) !== null && _a !== void 0 ? _a : '?'}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const existingByType = this.messagesByMsgType.get(msgType);
|
|
175
|
+
if (existingByType) {
|
|
176
|
+
this.addError('DUPLICATE_MESSAGE_TYPE', `Duplicate message type '${msgType}' for message '${name}'. Type already used by '${existingByType.name}'`, name, 'message', lineNumber);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const info = { name, msgType, lineNumber };
|
|
180
|
+
this.messagesByName.set(name, info);
|
|
181
|
+
this.messagesByMsgType.set(msgType, info);
|
|
182
|
+
this.validateFieldReferences(message, name, 'message');
|
|
183
|
+
}
|
|
184
|
+
validateHeader(doc) {
|
|
185
|
+
const header = doc.firstDescendant('header');
|
|
186
|
+
if (!header) {
|
|
187
|
+
this.addError('MISSING_HEADER', 'No <header> section found in dictionary');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.validateFieldReferences(header, 'StandardHeader', 'header');
|
|
191
|
+
}
|
|
192
|
+
validateTrailer(doc) {
|
|
193
|
+
const trailer = doc.firstDescendant('trailer');
|
|
194
|
+
if (!trailer) {
|
|
195
|
+
this.addError('MISSING_TRAILER', 'No <trailer> section found in dictionary');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.validateFieldReferences(trailer, 'StandardTrailer', 'trailer');
|
|
199
|
+
}
|
|
200
|
+
validateComponentReferences(doc) {
|
|
201
|
+
const componentsNode = doc.firstDescendant('components');
|
|
202
|
+
if (!componentsNode)
|
|
203
|
+
return;
|
|
204
|
+
for (const component of componentsNode.elements('component')) {
|
|
205
|
+
const name = component.attribute('name');
|
|
206
|
+
if (!name)
|
|
207
|
+
continue;
|
|
208
|
+
for (const compRef of component.descendants('component')) {
|
|
209
|
+
this.validateComponentReference(compRef, name, 'component');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
validateMessageReferences(doc) {
|
|
214
|
+
const messagesNode = doc.firstDescendant('messages');
|
|
215
|
+
if (!messagesNode)
|
|
216
|
+
return;
|
|
217
|
+
for (const message of messagesNode.elements('message')) {
|
|
218
|
+
const name = message.attribute('name');
|
|
219
|
+
if (!name)
|
|
220
|
+
continue;
|
|
221
|
+
for (const compRef of message.descendants('component')) {
|
|
222
|
+
this.validateComponentReference(compRef, name, 'message');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
validateFieldReferences(container, containerName, containerType) {
|
|
227
|
+
var _a;
|
|
228
|
+
for (const fieldRef of container.elements('field')) {
|
|
229
|
+
const fieldName = fieldRef.attribute('name');
|
|
230
|
+
const lineNumber = fieldRef.line;
|
|
231
|
+
if (!fieldName) {
|
|
232
|
+
this.addError('FIELD_REF_NO_NAME', `Field reference in ${containerType} '${containerName}' missing 'name' attribute`, containerName, containerType, lineNumber);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
this.referencedFields.add(fieldName);
|
|
236
|
+
if (!this.fieldsByName.has(fieldName)) {
|
|
237
|
+
const correctCase = this.fieldNamesCaseInsensitive.get(fieldName.toLowerCase());
|
|
238
|
+
const suggestion = correctCase !== null && correctCase !== void 0 ? correctCase : DictionaryValidator.findSimilar(fieldName, this.allFieldNames);
|
|
239
|
+
this.addError('UNDEFINED_FIELD', `Field '${fieldName}' referenced in ${containerType} '${containerName}' is not defined`, fieldName, 'field reference', lineNumber, suggestion != null ? `Did you mean '${suggestion}'?` : 'Add the field to the <fields> section');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const group of container.elements('group')) {
|
|
243
|
+
const groupName = (_a = group.attribute('name')) !== null && _a !== void 0 ? _a : 'unknown';
|
|
244
|
+
this.referencedFields.add(groupName);
|
|
245
|
+
if (groupName !== 'unknown' && !this.fieldsByName.has(groupName)) {
|
|
246
|
+
const lineNumber = group.line;
|
|
247
|
+
const suggestion = DictionaryValidator.findSimilar(groupName, this.allFieldNames);
|
|
248
|
+
this.addError('UNDEFINED_GROUP_FIELD', `Group '${groupName}' in ${containerType} '${containerName}' has no corresponding field definition (for the repeating count)`, groupName, 'group', lineNumber, suggestion != null ? `Did you mean '${suggestion}'?` : 'Add a NUMINGROUP field for this group');
|
|
249
|
+
}
|
|
250
|
+
this.validateFieldReferences(group, `${containerName}.${groupName}`, 'group');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
validateComponentReference(compRef, containerName, containerType) {
|
|
254
|
+
const compName = compRef.attribute('name');
|
|
255
|
+
const lineNumber = compRef.line;
|
|
256
|
+
if (!compName) {
|
|
257
|
+
this.addError('COMPONENT_REF_NO_NAME', `Component reference in ${containerType} '${containerName}' missing 'name' attribute`, containerName, containerType, lineNumber);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.referencedComponents.add(compName);
|
|
261
|
+
if (!this.componentsByName.has(compName) &&
|
|
262
|
+
compName !== 'StandardHeader' && compName !== 'StandardTrailer') {
|
|
263
|
+
const suggestion = DictionaryValidator.findSimilar(compName, this.allComponentNames);
|
|
264
|
+
this.addError('UNDEFINED_COMPONENT', `Component '${compName}' referenced in ${containerType} '${containerName}' is not defined`, compName, 'component reference', lineNumber, suggestion != null ? `Did you mean '${suggestion}'?` : 'Add the component to the <components> section');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
checkUnusedDefinitions() {
|
|
268
|
+
for (const field of this.fieldsByName.values()) {
|
|
269
|
+
if (!this.referencedFields.has(field.name)) {
|
|
270
|
+
this.addWarning('UNUSED_FIELD', `Field '${field.name}' (tag ${field.tag}) is defined but never referenced`, field.name, 'field', field.lineNumber);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const comp of this.componentsByName.values()) {
|
|
274
|
+
if (!this.referencedComponents.has(comp.name)) {
|
|
275
|
+
this.addWarning('UNUSED_COMPONENT', `Component '${comp.name}' is defined but never referenced`, comp.name, 'component', comp.lineNumber);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
addError(code, message, elementName, elementType, lineNumber, suggestion) {
|
|
280
|
+
this._errors.push({ severity: validation_error_1.ValidationSeverity.Error, code, message, elementName, elementType, lineNumber, suggestion });
|
|
281
|
+
}
|
|
282
|
+
addWarning(code, message, elementName, elementType, lineNumber, suggestion) {
|
|
283
|
+
this._errors.push({ severity: validation_error_1.ValidationSeverity.Warning, code, message, elementName, elementType, lineNumber, suggestion });
|
|
284
|
+
}
|
|
285
|
+
static findSimilar(input, candidates) {
|
|
286
|
+
let bestMatch = null;
|
|
287
|
+
let bestDistance = Infinity;
|
|
288
|
+
const maxDistance = Math.max(3, Math.floor(input.length / 2));
|
|
289
|
+
const inputLower = input.toLowerCase();
|
|
290
|
+
for (const candidate of candidates) {
|
|
291
|
+
const distance = DictionaryValidator.levenshteinDistance(inputLower, candidate.toLowerCase());
|
|
292
|
+
if (distance < bestDistance && distance <= maxDistance) {
|
|
293
|
+
bestDistance = distance;
|
|
294
|
+
bestMatch = candidate;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return bestMatch;
|
|
298
|
+
}
|
|
299
|
+
static levenshteinDistance(s1, s2) {
|
|
300
|
+
const n = s1.length;
|
|
301
|
+
const m = s2.length;
|
|
302
|
+
if (n === 0)
|
|
303
|
+
return m;
|
|
304
|
+
if (m === 0)
|
|
305
|
+
return n;
|
|
306
|
+
const d = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
307
|
+
for (let i = 0; i <= n; i++)
|
|
308
|
+
d[i][0] = i;
|
|
309
|
+
for (let j = 0; j <= m; j++)
|
|
310
|
+
d[0][j] = j;
|
|
311
|
+
for (let i = 1; i <= n; i++) {
|
|
312
|
+
for (let j = 1; j <= m; j++) {
|
|
313
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
314
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return d[n][m];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
exports.DictionaryValidator = DictionaryValidator;
|
|
321
|
+
//# sourceMappingURL=dictionary-validator.js.map
|