diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 4136590..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,58 +0,0 @@ -/* -👋 Hi! This file was autogenerated by tslint-to-eslint-config. -https://github.com/typescript-eslint/tslint-to-eslint-config - -It represents the closest reasonable ESLint configuration to this -project's original TSLint configuration. - -We recommend eventually switching this configuration to extend from -the recommended rulesets in typescript-eslint. -https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md - -Happy linting! 💖 -*/ -module.exports = { - "env": { - "es6": true, - "node": true - }, - "extends": [ - "prettier", - "prettier/@typescript-eslint" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/member-delimiter-style": [ - "warn", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "semi", - "requireLast": false - } - } - ], - "@typescript-eslint/no-unused-expressions": "warn", - "@typescript-eslint/semi": [ - "warn", - "always" - ], - "curly": "warn", - "eqeqeq": [ - "warn", - "always" - ], - "no-redeclare": "warn", - "no-throw-literal": "warn" - } -}; diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 066e901..e52cd21 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -26,6 +26,14 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present -# - run: npm test -# env: -# CI: true + - run: sudo apt-get update && sudo apt-get install -y xvfb + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1280x1024x24 -ac &>/dev/null & + sleep 1 + shell: bash + - name: Run tests + run: npm test + env: + DISPLAY: :99 + CI: true diff --git a/.npmrc b/.npmrc index 11a5949..e69de29 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -scripts-prepend-node-path=auto diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..abc1cda --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +out +.vscode-test +*.min.js +package-lock.json +orchestrations +images diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9a8f729 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "useTabs": true, + "trailingComma": "es5", + "printWidth": 120, + "arrowParens": "avoid" +} diff --git a/doc/RAW_FIX_CONVERTER.md b/doc/RAW_FIX_CONVERTER.md new file mode 100644 index 0000000..65ec3a7 --- /dev/null +++ b/doc/RAW_FIX_CONVERTER.md @@ -0,0 +1,167 @@ +# Convert Pretty-Printed FIX to Raw FIX Format + +This guide shows you how to use the new "Convert to Raw FIX" commands to transform pretty-printed FIX messages back into their raw protocol format. + +## What's New + +Two new commands have been added: +- **FIX Master - Convert to Raw FIX**: Converts entire document from pretty-printed to raw FIX +- **FIX Master - Convert Selection to Raw FIX**: Converts only selected text + +## How It Works + +The commands parse pretty-printed FIX messages like this: + +``` +NewOrderSingle +{ + BeginString (8) FIX.4.4 + BodyLength (9) 140 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + MsgSeqNum (34) 2282 + SendingTime (52) 20190929-04:51:00.849 + ClOrdID (11) 50 + AllocAccount (70) 49 + ExDestination (100) AUTO + Symbol (55) WTF + Side (54) 1 - Buy + TransactTime (60) 20190929-04:35:33.562 + OrderQty (38) 10000 + OrdType (40) 1 - Market + TimeInForce (59) 1 - GoodTillCancel + CheckSum (10) 129 +} +``` + +And converts them to raw FIX format: + +``` +8=FIX.4.4�9=140�35=D�49=INITIATOR�56=ACCEPTOR�34=2282�52=20190929-04:51:00.849�11=50�70=49�100=AUTO�55=WTF�54=1�60=20190929-04:35:33.562�38=10000�40=1�59=1�10=129� +``` + +(Note: `�` represents the SOH character `0x01` which is the standard FIX field delimiter) + +## Usage + +### Method 1: Command Palette + +1. Open a file with pretty-printed FIX messages (usually a `.fix` file) +2. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +3. Type "FIX Master - Convert to Raw FIX" +4. Press Enter +5. A new document opens in a side-by-side view with the raw FIX output + +### Method 2: Convert Selection Only + +1. Select one or more pretty-printed messages +2. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +3. Type "FIX Master - Convert Selection to Raw FIX" +4. Press Enter +5. A new document opens with the converted selection + +### Method 3: Right-Click Context Menu + +1. Select the text you want to convert +2. Right-click to open the context menu +3. Choose "FIX Master - Convert Selection to Raw FIX" + +## Testing the Feature + +### Step 1: Create a Test File + +1. In VS Code, press `F5` to launch the Extension Development Host +2. Create a new file with this content: + +``` +NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + ClOrdID (11) ORDER123 + Side (54) 1 - Buy + Symbol (55) AAPL + OrderQty (38) 100 + OrdType (40) 2 - Limit + Price (44) 150.50 +} +``` + +### Step 2: Run the Command + +1. Press `Cmd+Shift+P` / `Ctrl+Shift+P` +2. Run "FIX Master - Convert to Raw FIX" +3. You should see a new document with raw FIX output + +### Step 3: Verify Round-Trip + +To verify the conversion works correctly: + +1. Copy the raw FIX output from the new document +2. Create another new file and paste the raw FIX +3. Run "FIX Master - Pretty Print" +4. Compare with the original pretty-printed message - they should match! + +## Features + +### Multiple Messages + +The command handles multiple messages in the same document: + +``` +Logon +{ + BeginString (8) FIX.4.4 + ... +} + +NewOrderSingle +{ + BeginString (8) FIX.4.4 + ... +} +``` + +Both messages will be converted to raw FIX format. + +### Preserves Non-Message Lines + +Lines outside message blocks (like headers, timestamps, or notes) are preserved: + +``` +=== Trading Session 2024-01-15 === + +NewOrderSingle +{ + ... +} + +=== End of Session === +``` + +### Data Fields + +Base64-encoded data fields (like Signature, SecureData) are automatically decoded back to their binary representation. + +## Use Cases + +1. **Message Replay**: Convert formatted messages back to raw FIX for replaying to a test environment +2. **Debugging**: Compare raw messages before and after formatting +3. **Log Analysis**: Extract specific messages from logs and convert them for injection into test systems +4. **Message Modification**: Edit pretty-printed messages and convert back to raw FIX +5. **Integration Testing**: Generate raw FIX messages from human-readable format + +## Implementation Details + +- Parser: `parsePrettyPrintedMessage()` in `src/fixProtocol.ts` +- Formatter: `fixFormatPrintMessage()` in `src/fixProtocol.ts` +- Command Handler: `formatRawFix()` in `src/extension.ts` + +## Limitations + +- The converter relies on the tag numbers in parentheses to extract field values +- Enumerated values (e.g., "1 - Buy") are correctly parsed to extract just the value ("1") +- The output is always in a new document (doesn't support in-place replacement) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b47ac2a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,50 @@ +const tsPlugin = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + sourceType: 'module', + }, + globals: { + console: 'readonly', + process: 'readonly', + __dirname: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + // TypeScript specific rules + '@typescript-eslint/no-unused-expressions': 'warn', + + // General JavaScript/TypeScript rules + // Note: semi, quotes, indent rules are handled by Prettier + 'curly': 'warn', + 'eqeqeq': ['warn', 'always'], + 'no-redeclare': 'warn', + 'no-throw-literal': 'warn', + + // Disable rules that conflict with Prettier + ...prettierConfig.rules, + }, + }, + { + ignores: [ + 'node_modules/**', + 'out/**', + '.vscode-test/**', + '*.js', + '!eslint.config.js', + ], + }, +]; diff --git a/package-lock.json b/package-lock.json index 1533dda..8d0a851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,10 +21,14 @@ "@types/mocha": "^10.0.1", "@types/node": "^20.12.7", "@types/vscode": "^1.88.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vscode/test-electron": "^2.3.4", "eslint": "^9.1.0", + "eslint-config-prettier": "^10.1.8", "glob": "^11.1.0", "mocha": "^10.8.2", + "prettier": "^3.7.4", "ts-loader": "^9.4.4", "typescript": "^5.1.6", "webpack": "^5.94.0", @@ -53,16 +57,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -388,6 +396,263 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vscode/test-electron": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", @@ -1220,6 +1485,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", @@ -1237,10 +1518,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1551,6 +1833,13 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2418,6 +2707,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2944,6 +3249,54 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2956,6 +3309,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", diff --git a/package.json b/package.json index af5bdb8..766e1c4 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,14 @@ "command": "extension.format-csv-selection", "title": "FIX Master - CSV Format Selection" }, + { + "command": "extension.format-raw-fix", + "title": "FIX Master - Convert to Raw FIX" + }, + { + "command": "extension.format-raw-fix-selection", + "title": "FIX Master - Convert Selection to Raw FIX" + }, { "command": "extension.show-field", "title": "FIX Master - Show the definition of a field" @@ -129,6 +137,13 @@ "when": "editorHasSelection", "command": "extension.format-csv-selection" }, + { + "command": "extension.format-raw-fix" + }, + { + "when": "editorHasSelection", + "command": "extension.format-raw-fix-selection" + }, { "command": "extension.show-field" } @@ -148,6 +163,11 @@ "when": "editorHasSelection", "command": "extension.format-csv-selection", "group": "1_modification" + }, + { + "when": "editorHasSelection", + "command": "extension.format-raw-fix-selection", + "group": "1_modification" } ] }, @@ -247,16 +267,22 @@ "watch": "tsc -watch -p ./", "pretest": "npm run compile", "test": "node ./out/test/runTest.js", - "lint": "eslint -c .eslintrc.js --ext .ts ./" + "lint": "eslint .", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"" }, "devDependencies": { "@types/mocha": "^10.0.1", "@types/node": "^20.12.7", "@types/vscode": "^1.88.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vscode/test-electron": "^2.3.4", "eslint": "^9.1.0", + "eslint-config-prettier": "^10.1.8", "glob": "^11.1.0", "mocha": "^10.8.2", + "prettier": "^3.7.4", "ts-loader": "^9.4.4", "typescript": "^5.1.6", "webpack": "^5.94.0", diff --git a/src/definitions.ts b/src/definitions.ts index ff6ae79..978be30 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -67,7 +67,7 @@ export class Field { public toString = () : string => { return `Field(${this.name}=${this.tag})`; - } + }; } // diff --git a/src/extension.ts b/src/extension.ts index 4549af4..160d102 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { Orchestra } from './fixOrchestra'; import { DataDictionary } from './quickFixDataDictionary'; -import { fixMessagePrefix, parseMessage, prettyPrintMessage, msgTypeHeartbeat, msgTypeTestRequest, csvPrintMessage, Message } from './fixProtocol'; +import { fixMessagePrefix, parseMessage, prettyPrintMessage, msgTypeHeartbeat, msgTypeTestRequest, csvPrintMessage, fixFormatPrintMessage, parsePrettyPrintedMessage, Message } from './fixProtocol'; import { AdministrativeMessageBehaviour, CommandScope, EditorReuse, NameLookup } from './options'; import { definitionHtmlForField } from './html'; import { OrderBook } from './orderBook'; @@ -25,23 +25,23 @@ export function activate(context: ExtensionContext) { } return workspace.workspaceFolders[0].uri.path; - } + }; const resolvePathVariables = (path: string, context: string) : string | undefined => { - if (path.search(/\${workspaceFolder}/g) == -1) { + if (path.search(/\${workspaceFolder}/g) === -1) { return path; } let workspaceFolder = getWorkspaceFolder(); - if (workspaceFolder == undefined) { + if (workspaceFolder === undefined) { window.showErrorMessage("Unable to determine ${workspaceFolder} when resolving the " + context + " path."); return undefined; } - return path.replace(/\${workspaceFolder}/g, workspaceFolder) - } + return path.replace(/\${workspaceFolder}/g, workspaceFolder); + }; const loadOrchestra = () => { @@ -55,7 +55,7 @@ export function activate(context: ExtensionContext) { orchestraPath = resolvePathVariables(orchestraPath, "orchestra"); - if (orchestraPath == undefined) { + if (orchestraPath === undefined) { return; } @@ -65,7 +65,7 @@ export function activate(context: ExtensionContext) { if (!fs.existsSync(orchestraPath)) { window.showErrorMessage("The orchestra path '" + orchestraPath + "' cannot be found."); - return + return; } window.withProgress({ @@ -99,7 +99,7 @@ export function activate(context: ExtensionContext) { path = resolvePathVariables(path, "QuickFix data dictionary"); - if (path == undefined) { + if (path === undefined) { return; } @@ -202,19 +202,19 @@ export function activate(context: ExtensionContext) { let getDocument = async (editorReuse:EditorReuse) : Promise => { - if (editorReuse != EditorReuse.New) { - const document = workspace.textDocuments.find(document => { return document.languageId === 'FIX' }) + if (editorReuse !== EditorReuse.New) { + const document = workspace.textDocuments.find(document => { return document.languageId === 'FIX'; }); if (document) { if (editorReuse === EditorReuse.Replace) { let edit = new WorkspaceEdit(); var firstLine = document.lineAt(0); var lastLine = document.lineAt(document.lineCount - 1); var textRange = new Range(firstLine.range.start, lastLine.range.end); - edit.delete(document.uri, textRange) + edit.delete(document.uri, textRange); await workspace.applyEdit(edit); } - await window.showTextDocument(document) - return document + await window.showTextDocument(document); + return document; } } @@ -248,17 +248,17 @@ export function activate(context: ExtensionContext) { const configuration = workspace.getConfiguration(); const editorReuse = EditorReuse[configuration.get("fixmaster.editorReuse") as keyof typeof EditorReuse]; - let document = await getDocument(editorReuse) + let document = await getDocument(editorReuse); let edit = new WorkspaceEdit(); var index = 0; if (editorReuse === EditorReuse.Append) { - index = document.lineCount - 1 + index = document.lineCount - 1; } - if (scope == CommandScope.Document) { + if (scope === CommandScope.Document) { edit.insert(document.uri, new Position(index, 0), sourceDocument.getText()); } else { @@ -421,7 +421,80 @@ export function activate(context: ExtensionContext) { commands.registerCommand('extension.format-csv-selection', async () => { await format(csvPrintMessage, CommandScope.Selection); }); - + + commands.registerCommand('extension.format-raw-fix', async () => { + await formatRawFix(CommandScope.Document); + }); + + commands.registerCommand('extension.format-raw-fix-selection', async () => { + await formatRawFix(CommandScope.Selection); + }); + + let formatRawFix = async (scope: CommandScope) => { + if (!orchestra) { + window.showErrorMessage('The orchestra has not been loaded - check the orchestraPath setting.'); + return; + } + + const {activeTextEditor} = window; + + if (!activeTextEditor) { + window.showErrorMessage('No document is open or the file is too large.'); + return; + } + + const sourceDocument = activeTextEditor.document; + let text: string; + + if (scope === CommandScope.Document) { + text = sourceDocument.getText(); + } else { + const selection = activeTextEditor.selection; + if (!selection) { + window.showErrorMessage('No text selected.'); + return; + } + const range = new Range(selection.start, selection.end); + text = sourceDocument.getText(range); + } + + // Parse pretty-printed messages and convert to raw FIX + const lines = text.split('\n'); + let output = ''; + let currentMessageLines: string[] = []; + let inMessage = false; + + for (const line of lines) { + if (line.trim() === '{') { + inMessage = true; + currentMessageLines = [line]; + } else if (line.trim() === '}') { + currentMessageLines.push(line); + inMessage = false; + + // Parse this message block + const messageText = currentMessageLines.join('\n'); + const message = parsePrettyPrintedMessage(messageText); + + if (message) { + const rawFix = fixFormatPrintMessage('', message, orchestra, dataDictionary, 0); + output += rawFix; + } + + currentMessageLines = []; + } else if (inMessage) { + currentMessageLines.push(line); + } else if (!inMessage && line.trim().length > 0) { + // Preserve non-message lines (like headers, timestamps, etc.) + output += line + '\n'; + } + } + + // Create a new document with the raw FIX output + const document = await workspace.openTextDocument({ content: output, language: 'plaintext' }); + await window.showTextDocument(document, ViewColumn.Beside); + }; + commands.registerCommand('extension.show-field', async () => { if (!orchestra) { diff --git a/src/fixOrchestra.ts b/src/fixOrchestra.ts index 51d3c90..5441bfc 100644 --- a/src/fixOrchestra.ts +++ b/src/fixOrchestra.ts @@ -15,7 +15,7 @@ export class Orchestra this.orchestrations = filenames.map(entry => { const orchestraPath = path.join(root, entry); try { - return new xml.Orchestration(orchestraPath) + return new xml.Orchestration(orchestraPath); } catch (err) { throw new Error(`failed to load orchestra file '${orchestraPath}' - ${err}`); } diff --git a/src/fixProtocol.ts b/src/fixProtocol.ts index 3a24d95..5b832be 100644 --- a/src/fixProtocol.ts +++ b/src/fixProtocol.ts @@ -316,14 +316,14 @@ export function csvPrintMessage(context: string, message: Message, orchestra: FI var buffer: string = ""; const description = message.describe(orchestra, quickFix); - + if (context && context.length > 0) { buffer += context + " "; } if (description.messageName && description.messageName.length > 0) { buffer += description.messageName + "\n"; - } + } buffer += "{\n"; @@ -342,3 +342,103 @@ export function csvPrintMessage(context: string, message: Message, orchestra: FI return buffer; } + +export function fixFormatPrintMessage(context: string, message: Message, orchestra: FIX.Orchestra, quickFix: DataDictionary | null, _: number) { + + var buffer: string = ""; + const description = message.describe(orchestra, quickFix); + + if (context && context.length > 0) { + buffer += context + " "; + } + + // Get the BeginString field to determine FIX version for data field handling + const beginString = message.fields.find(field => field.tag === beginStringTag); + var version: xml.Orchestration | undefined; + + if (beginString) { + version = orchestra.orchestrations.find(v => v.version === beginString.value); + } + + // Build the raw FIX message + message.fields.forEach((field, index) => { + buffer += `${field.tag}${fieldValueSeparator}`; + + // Check if this is a data field (base64 encoded) that needs decoding + let definition = orchestra.definitionOfField(field.tag, version, undefined); + if (definition?.field.type === 'data') { + // Decode base64 encoded data fields back to their original form + try { + const decoded = base64.decode(field.value); + buffer += decoded; + } catch (err) { + // If decoding fails, use the value as-is + buffer += field.value; + } + } else { + buffer += field.value; + } + + buffer += fieldDelimiter; + }); + + buffer += "\n"; + + return buffer; +} + +export function parsePrettyPrintedMessage(text: string): Message | null { + // Pattern to match pretty-printed fields: FieldName (tag) value + // or: FieldName (tag) value - description (for enumerated values) + const fieldPattern = /^\s*\w+\s+\((\d+)\)\s+(.+?)(?:\s+-\s+.+)?$/; + + // Normalize line endings to handle both Unix (\n) and Windows (\r\n) + const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); + const fields: Field[] = []; + var msgType: string | null = null; + var inMessage = false; + + for (const line of lines) { + // Check for message start + if (line.trim() === '{') { + inMessage = true; + continue; + } + + // Check for message end + if (line.trim() === '}') { + break; + } + + // Skip message name line (doesn't have parentheses) + if (!inMessage || !line.includes('(')) { + continue; + } + + const match = fieldPattern.exec(line); + if (match) { + const tag = parseInt(match[1]); + let value = match[2].trim(); + + // For enumerated values, extract just the value part before the dash + // Example: "1 - Buy" -> "1" + const dashIndex = value.indexOf(' - '); + if (dashIndex > 0) { + value = value.substring(0, dashIndex).trim(); + } + + const field = new Field(tag, value); + fields.push(field); + + if (tag === msgTypeTag) { + msgType = value; + } + } + } + + if (fields.length === 0) { + return null; + } + + return new Message(msgType, fields); +} diff --git a/src/test/suite/fixProtocol.test.ts b/src/test/suite/fixProtocol.test.ts index 3d058f9..67b82a7 100644 --- a/src/test/suite/fixProtocol.test.ts +++ b/src/test/suite/fixProtocol.test.ts @@ -8,7 +8,7 @@ import { Orchestra } from '../../fixOrchestra'; import * as vscode from 'vscode'; // import * as myExtension from '../extension'; -import { parseMessage } from '../../fixProtocol'; +import { parseMessage, fixFormatPrintMessage, parsePrettyPrintedMessage, prettyPrintMessage } from '../../fixProtocol'; suite('FIX Protocol Test Suite', () => { @@ -80,7 +80,7 @@ suite('FIX Protocol Test Suite', () => { test('Parse message with a data field', () => { let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); - let text = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; + let text = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; let message = parseMessage(text, orchestra); if (!message) { assert.fail("message failed to parse"); @@ -90,4 +90,376 @@ suite('FIX Protocol Test Suite', () => { let signature = message.fields[18]; assert.equal('QUJDREVGAUFCQ0RFRkFCQwFERUY=', signature.value); }); + + test('fixFormatPrintMessage outputs raw FIX format', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + // Remove the trailing newline for comparison + output = output.trimEnd(); + assert.equal(text, output); + }); + + test('fixFormatPrintMessage with context prefix', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let context = "2024-01-15 10:30:45.123"; + let output = fixFormatPrintMessage(context, message, orchestra, null, 0); + // Check that context appears at the beginning + assert.ok(output.startsWith(context + " ")); + // Check that the FIX message follows + assert.ok(output.includes("8=FIX.4.4")); + }); + + test('fixFormatPrintMessage with NewOrderSingle', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=140\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2282\u000152=20190929-04:51:00.849\u000111=50\u000170=49\u0001100=AUTO\u000155=WTF\u000154=1\u000160=20190929-04:35:33.562\u000138=10000\u000140=1\u000159=1\u000110=129\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + assert.equal(text, output); + }); + + test('fixFormatPrintMessage with data field decodes base64', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalText = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; + let message = parseMessage(originalText, orchestra); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + // The output should match the original since data fields are decoded from base64 + assert.equal(originalText, output); + }); + + test('fixFormatPrintMessage round-trip preserves message', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalText = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + + // Parse the message + let message = parseMessage(originalText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + + // Convert back to FIX format + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + + // Parse again + let reparsed = parseMessage(output); + if (!reparsed) { + assert.fail("reparsed message failed to parse"); + return; + } + + // Check that field counts match + assert.equal(message.fields.length, reparsed.fields.length); + + // Check that all field values match + for (let i = 0; i < message.fields.length; i++) { + assert.equal(message.fields[i].tag, reparsed.fields[i].tag); + assert.equal(message.fields[i].value, reparsed.fields[i].value); + } + }); + + test('parsePrettyPrintedMessage parses basic message', () => { + let prettyText = `Logon +{ + BeginString (8) FIX.4.4 + BodyLength (9) 72 + MsgType (35) A + SenderCompID (49) ACCEPTOR + TargetCompID (56) INITIATOR + MsgSeqNum (34) 1 + SendingTime (52) 20190816-10:34:27.742 + EncryptMethod (98) 0 + HeartBtInt (108) 30 + CheckSum (10) 012 +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(10, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[2].tag); + assert.equal("A", message.fields[2].value); + assert.equal("A", message.msgType); + }); + + test('parsePrettyPrintedMessage handles enumerated values', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + Side (54) 1 - Buy + OrdType (40) 2 - Limit +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(4, message.fields.length); + // Check that enumerated values are parsed correctly (should extract "1" from "1 - Buy") + let sideField = message.fields.find(f => f.tag === 54); + assert.ok(sideField); + assert.equal("1", sideField.value); + + let ordTypeField = message.fields.find(f => f.tag === 40); + assert.ok(ordTypeField); + assert.equal("2", ordTypeField.value); + }); + + test('parsePrettyPrintedMessage handles NewOrderSingle', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + ClOrdID (11) ORDER123 + Side (54) 1 - Buy + Symbol (55) AAPL + OrderQty (38) 100 + OrdType (40) 2 - Limit + Price (44) 150.50 +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(10, message.fields.length); + assert.equal("D", message.msgType); + + let clOrdIdField = message.fields.find(f => f.tag === 11); + assert.ok(clOrdIdField); + assert.equal("ORDER123", clOrdIdField.value); + + let priceField = message.fields.find(f => f.tag === 44); + assert.ok(priceField); + assert.equal("150.50", priceField.value); + }); + + test('parsePrettyPrintedMessage returns null for empty input', () => { + let prettyText = ``; + let message = parsePrettyPrintedMessage(prettyText); + assert.equal(null, message); + }); + + test('parsePrettyPrintedMessage returns null for no fields', () => { + let prettyText = `NewOrderSingle +{ +}`; + let message = parsePrettyPrintedMessage(prettyText); + assert.equal(null, message); + }); + + test('parsePrettyPrintedMessage handles message without header', () => { + let prettyText = `{ + BeginString (8) FIX.4.4 + MsgType (35) A +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(2, message.fields.length); + assert.equal("A", message.msgType); + }); + + test('parsePrettyPrintedMessage ignores non-field lines', () => { + let prettyText = `NewOrderSingle +{ + This is a comment line + BeginString (8) FIX.4.4 + Another comment + MsgType (35) D - NewOrderSingle + More comments here + Side (54) 1 - Buy +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + // Should only parse lines with (tag) pattern + assert.equal(3, message.fields.length); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage round-trip with prettyPrintMessage', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalRaw = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + + // Parse raw message + let originalMessage = parseMessage(originalRaw); + if (!originalMessage) { + assert.fail("original message failed to parse"); + return; + } + + // Convert to pretty print + let prettyText = prettyPrintMessage("", originalMessage, orchestra, null, 0); + + // Parse pretty printed back to message + let reparsedMessage = parsePrettyPrintedMessage(prettyText); + if (!reparsedMessage) { + assert.fail("reparsed message failed to parse"); + return; + } + + // Check that field counts match + assert.equal(originalMessage.fields.length, reparsedMessage.fields.length); + + // Check that all field values match + for (let i = 0; i < originalMessage.fields.length; i++) { + assert.equal(originalMessage.fields[i].tag, reparsedMessage.fields[i].tag); + assert.equal(originalMessage.fields[i].value, reparsedMessage.fields[i].value); + } + }); + + test('parsePrettyPrintedMessage full round-trip: raw -> pretty -> parse -> raw', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalRaw = "8=FIX.4.4\u00019=140\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2282\u000152=20190929-04:51:00.849\u000111=50\u000170=49\u0001100=AUTO\u000155=WTF\u000154=1\u000160=20190929-04:35:33.562\u000138=10000\u000140=1\u000159=1\u000110=129\u0001"; + + // Step 1: Parse raw FIX + let step1 = parseMessage(originalRaw); + if (!step1) { + assert.fail("step 1 failed"); + return; + } + + // Step 2: Convert to pretty print + let prettyText = prettyPrintMessage("", step1, orchestra, null, 0); + + // Step 3: Parse pretty printed + let step3 = parsePrettyPrintedMessage(prettyText); + if (!step3) { + assert.fail("step 3 failed"); + return; + } + + // Step 4: Convert back to raw FIX + let finalRaw = fixFormatPrintMessage("", step3, orchestra, null, 0); + finalRaw = finalRaw.trimEnd(); + + // Verify: original raw should equal final raw + assert.equal(originalRaw, finalRaw); + }); + + test('parsePrettyPrintedMessage handles fields with special characters', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + Symbol (55) ABC.XYZ + Text (58) Test order with spaces and symbols! +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + + let symbolField = message.fields.find(f => f.tag === 55); + assert.ok(symbolField); + assert.equal("ABC.XYZ", symbolField.value); + + let textField = message.fields.find(f => f.tag === 58); + assert.ok(textField); + assert.equal("Test order with spaces and symbols!", textField.value); + }); + + test('parsePrettyPrintedMessage normalizes Windows line endings (CRLF)', () => { + // Use \r\n (Windows) line endings + let prettyText = "NewOrderSingle\r\n{\r\n BeginString (8) FIX.4.4\r\n MsgType (35) D - NewOrderSingle\r\n Side (54) 1 - Buy\r\n}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with Windows line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage normalizes old Mac line endings (CR)', () => { + // Use \r (old Mac) line endings + let prettyText = "NewOrderSingle\r{\r BeginString (8) FIX.4.4\r MsgType (35) D - NewOrderSingle\r Side (54) 1 - Buy\r}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with old Mac line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage normalizes mixed line endings', () => { + // Mix of \r\n (Windows), \r (old Mac), and \n (Unix) + let prettyText = "NewOrderSingle\r\n{\r BeginString (8) FIX.4.4\n MsgType (35) D - NewOrderSingle\r\n Side (54) 1 - Buy\r}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with mixed line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage with Windows line endings round-trip', () => { + // Verify Windows line endings work with full message parsing + let prettyText = "Logon\r\n{\r\n BeginString (8) FIX.4.4\r\n BodyLength (9) 72\r\n MsgType (35) A\r\n SenderCompID (49) ACCEPTOR\r\n TargetCompID (56) INITIATOR\r\n MsgSeqNum (34) 1\r\n SendingTime (52) 20190816-10:34:27.742\r\n EncryptMethod (98) 0\r\n HeartBtInt (108) 30\r\n CheckSum (10) 012\r\n}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with Windows line endings"); + return; + } + assert.equal(10, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[2].tag); + assert.equal("A", message.fields[2].value); + assert.equal("A", message.msgType); + }); });