diff --git a/demo.html b/demo.html deleted file mode 100644 index 6a475dd..0000000 --- a/demo.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - FML Syntax Validation Demo - - - -
-

๐Ÿ” FML Syntax Validation Endpoint

- -

Implementation of FML (FHIR Mapping Language) syntax validation endpoint as requested in the issue. This provides detailed error messages with line and column information for debugging FML syntax errors.

- -
-

โœ… Valid FML Example

-
-

Input FML:

-
map "http://example.org/fhir/StructureMap/PatientTransform" = "PatientTransform" - -uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source -uses "http://example.org/StructureDefinition/MyPatient" alias MyPatient as target - -group Patient(source src : Patient, target tgt : MyPatient) { - src.name -> tgt.name; - src.gender -> tgt.gender; -}
-

Validation Response:

-
{ - "resourceType": "OperationOutcome", - "issue": [ - { - "severity": "information", - "code": "informational", - "diagnostics": "FML syntax is valid" - } - ] -}
-
-
- -
-

โŒ Invalid FML Example

-
-

Input FML (with syntax errors):

-
// Missing map declaration -uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - -group Test(source src { - // Missing closing paren and brace -}
-

Validation Response:

-
{ - "resourceType": "OperationOutcome", - "issue": [ - { - "severity": "error", - "code": "invalid", - "diagnostics": "FML content must start with a map declaration", - "location": ["line 1, column 1"] - } - ] -}
-
-
- -
-

๐Ÿš€ API Usage

-
-

REST API Endpoint:

-
POST /api/v1/fml/validate-syntax - -curl -X POST http://localhost:3000/api/v1/fml/validate-syntax \ - -H "Content-Type: application/json" \ - -d '{"fmlContent": "map \"test\" = \"Test\""}'
- -

MCP Tool:

-
Tool: validate-fml-syntax -Input: {"fmlContent": "map \"test\" = \"Test\""}
-
-
- -
-

๐Ÿ› ๏ธ Features Implemented

- -
- -
-

๐Ÿงช Test Results

-
-

Core Library Tests: 11/11 passing โœ…

-

REST API Tests: 8/8 passing โœ…

-

MCP Interface: Working โœ…

-
-
-
- - \ No newline at end of file diff --git a/docs/FML_SYNTAX_VALIDATION.md b/docs/FML_SYNTAX_VALIDATION.md deleted file mode 100644 index 45c8581..0000000 --- a/docs/FML_SYNTAX_VALIDATION.md +++ /dev/null @@ -1,242 +0,0 @@ -# FML Syntax Validation Endpoint - -This document describes the FML (FHIR Mapping Language) syntax validation endpoint implementation added to the FML Runner library. - -## Overview - -The FML syntax validation endpoint provides comprehensive syntax checking for FHIR Mapping Language content without performing full compilation. It returns detailed error messages with line and column information to help developers debug FML syntax issues. - -## API Endpoints - -### REST API - -#### POST /api/v1/fml/validate-syntax - -Validates FML syntax and returns detailed validation results. - -**Request Body:** -```json -{ - "fmlContent": "map \"http://example.org/fhir/StructureMap/Test\" = \"Test\"\n\ngroup Test(source src, target tgt) {\n src.name -> tgt.name;\n}" -} -``` - -**Response (Valid FML):** -```json -{ - "resourceType": "OperationOutcome", - "issue": [ - { - "severity": "information", - "code": "informational", - "diagnostics": "FML syntax is valid" - } - ] -} -``` - -**Response (Invalid FML):** -```json -{ - "resourceType": "OperationOutcome", - "issue": [ - { - "severity": "error", - "code": "invalid", - "diagnostics": "FML content must start with a map declaration", - "location": ["line 1, column 1"] - } - ] -} -``` - -### MCP Interface - -#### validate-fml-syntax Tool - -**Input Schema:** -```json -{ - "type": "object", - "properties": { - "fmlContent": { - "type": "string", - "description": "FML content to validate" - } - }, - "required": ["fmlContent"] -} -``` - -**Response:** -```json -{ - "content": [ - { - "type": "text", - "text": "{\"valid\": true, \"errors\": [], \"warnings\": []}" - } - ] -} -``` - -## Core Library Usage - -```typescript -import { FmlRunner } from 'fmlrunner'; - -const runner = new FmlRunner(); -const result = runner.validateFmlSyntax(fmlContent); - -console.log('Valid:', result.valid); -console.log('Errors:', result.errors); -console.log('Warnings:', result.warnings); -``` - -## Features - -### Error Detection -- **Empty Content**: Detects when FML content is empty or whitespace-only -- **Missing Map Declaration**: Ensures FML starts with a map declaration -- **Bracket Validation**: Detects unmatched braces `{}` and parentheses `()` -- **Keyword Validation**: Validates presence of required FML keywords -- **Tokenization Errors**: Catches lexical analysis errors -- **Parse Errors**: Identifies structural parsing issues - -### Warning System -- **Missing Groups**: Warns when no group definitions are found -- **Best Practices**: Additional warnings for FML best practices - -### Comment Handling -- **Single-line Comments**: Properly handles `//` comments -- **Multi-line Comments**: Supports `/* */` comment blocks -- **Comment Skipping**: Comments are ignored during validation - -### Detailed Error Information -Each error includes: -- **Line Number**: The line where the error occurred -- **Column Number**: The column position of the error -- **Error Message**: Human-readable description of the issue -- **Error Code**: Structured error code for programmatic handling - -## Error Codes - -| Code | Description | -|------|-------------| -| `EMPTY_CONTENT` | FML content is empty or contains only whitespace | -| `MISSING_MAP_DECLARATION` | FML does not start with a map declaration | -| `TOKENIZATION_ERROR` | Error during lexical analysis | -| `PARSE_ERROR` | Error during structural parsing | -| `UNMATCHED_BRACE` | Closing brace without matching opening brace | -| `UNMATCHED_PAREN` | Closing parenthesis without matching opening parenthesis | -| `UNCLOSED_BRACE` | Opening brace without matching closing brace | -| `UNCLOSED_PAREN` | Opening parenthesis without matching closing parenthesis | -| `MISSING_MAP` | Required map declaration is missing | - -## Warning Codes - -| Code | Description | -|------|-------------| -| `NO_GROUPS` | No group definitions found in the FML | - -## Examples - -### Valid FML -```fml -map "http://example.org/fhir/StructureMap/PatientTransform" = "PatientTransform" - -uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source -uses "http://example.org/StructureDefinition/MyPatient" alias MyPatient as target - -group Patient(source src : Patient, target tgt : MyPatient) { - src.name -> tgt.name; - src.gender -> tgt.gender; - src.birthDate -> tgt.birthDate; -} -``` - -### Invalid FML Examples - -#### Missing Map Declaration -```fml -// Missing map declaration -uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - -group Test(source src : Patient, target tgt) { - src.name -> tgt.name; -} -``` -**Error**: `MISSING_MAP_DECLARATION` at line 1, column 1 - -#### Unmatched Braces -```fml -map "http://example.org/fhir/StructureMap/Test" = "Test" - -group Test(source src, target tgt) { - src.name -> tgt.name; - // Missing closing brace -``` -**Error**: `UNCLOSED_BRACE` - -#### Unmatched Parentheses -```fml -map "http://example.org/fhir/StructureMap/Test" = "Test" - -group Test(source src, target tgt { - src.name -> tgt.name; -} -``` -**Error**: `UNCLOSED_PAREN` - -## Implementation Details - -### Core Types - -```typescript -interface FmlSyntaxValidationResult { - valid: boolean; - errors: FmlSyntaxError[]; - warnings?: FmlSyntaxWarning[]; -} - -interface FmlSyntaxError { - line: number; - column: number; - message: string; - severity: 'error'; - code?: string; -} - -interface FmlSyntaxWarning { - line: number; - column: number; - message: string; - severity: 'warning'; - code?: string; -} -``` - -### Packages Modified - -1. **Core Library** (`packages/fmlrunner`): - - Added syntax validation types - - Implemented `validateSyntax()` in `FmlCompiler` - - Added `validateFmlSyntax()` to `FmlRunner` - -2. **REST API** (`packages/fmlrunner-rest`): - - Added `/api/v1/fml/validate-syntax` endpoint - - FHIR OperationOutcome responses - -3. **MCP Interface** (`packages/fmlrunner-mcp`): - - Added `validate-fml-syntax` tool - - JSON schema validation - -## Testing - -The implementation includes comprehensive test coverage: - -- **Core Library Tests**: 11/11 tests passing -- **REST API Tests**: 8/8 tests passing -- **MCP Interface**: Manual testing confirms functionality - -All tests verify error detection, warning generation, line/column reporting, and proper handling of edge cases. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8ca23d7..f9cd3ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,7 @@ "workspaces": [ "packages/fmlrunner", "packages/fmlrunner-rest", - "packages/fmlrunner-mcp", - "packages/fmlrunner-kotlin-core" + "packages/fmlrunner-mcp" ], "devDependencies": { "@types/jest": "^29.0.0", @@ -1185,9 +1184,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@litlfred/fmlrunner-core": { - "resolved": "packages/fmlrunner-kotlin-core", - "link": true + "node_modules/@lhncbc/ucum-lhc": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@lhncbc/ucum-lhc/-/ucum-lhc-5.0.4.tgz", + "integrity": "sha512-khuV9GV51DF80b0wJmhZTR5Bf23fhS6SSIWnyGT9X+Uvn0FsHFl2LKViQ2TTOuvwagUOUSq8/0SyoE2ZDGwrAA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "coffeescript": "^2.7.0", + "csv-parse": "^4.4.6", + "csv-stringify": "^1.0.4", + "escape-html": "^1.0.3", + "is-integer": "^1.0.6", + "jsonfile": "^2.2.3", + "stream": "0.0.2", + "stream-transform": "^0.1.1", + "string-to-stream": "^1.1.0", + "xmldoc": "^0.4.0" + } + }, + "node_modules/@loxjs/url-join": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@loxjs/url-join/-/url-join-1.0.2.tgz", + "integrity": "sha512-BqzK8+iHqxUbPRZV6NBum63CJzE0G6vGG3o+4dqeIzbywdoTg+xHJbksYDkk1P1w3Gj64U20Rgp44HHciLbRzg==", + "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { "version": "0.5.0", @@ -2057,6 +2076,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antlr4": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.9.3.tgz", + "integrity": "sha512-qNy2odgsa0skmNMCuxzXhM4M8J1YDaPv3TI+vCdnOAanu0N982wBrSqziDKRDctEZLZy9VffqIZXc0UGjjSP/g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2526,6 +2554,19 @@ "node": ">= 0.12.0" } }, + "node_modules/coffeescript": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz", + "integrity": "sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==", + "license": "MIT", + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -2610,6 +2651,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -2744,6 +2791,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2794,6 +2847,27 @@ "node": ">= 8" } }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "license": "MIT" + }, + "node_modules/csv-stringify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-1.1.2.tgz", + "integrity": "sha512-3NmNhhd+AkYs5YtM1GEh01VR6PKj6qch2ayfQaltx5xpcAdThjnbbI5eT8CzRVpXfGKAxnmrSYLsNl/4f3eWiw==", + "license": "BSD-3-Clause", + "dependencies": { + "lodash.get": "~4.4.2" + } + }, + "node_modules/date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2964,6 +3038,14 @@ "dev": true, "license": "MIT" }, + "node_modules/emitter-component": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", + "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -3217,7 +3299,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -3512,6 +3593,49 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fhirpath": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fhirpath/-/fhirpath-4.6.0.tgz", + "integrity": "sha512-nfK0+9mVLS/hyZNmwGlRV6EG8lll9VV5AGgAiXcCfSUms/M9R94JqyC34r3/Yjkp0ICuR70NH7Q7q9A2T91DzA==", + "hasInstallScript": true, + "license": "SEE LICENSE in LICENSE.md", + "dependencies": { + "@lhncbc/ucum-lhc": "^5.0.0", + "@loxjs/url-join": "^1.0.2", + "antlr4": "~4.9.3", + "commander": "^2.18.0", + "date-fns": "^1.30.1", + "js-yaml": "^3.13.1" + }, + "bin": { + "fhirpath": "bin/fhirpath" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/fhirpath/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/fhirpath/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4031,7 +4155,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/graphemer": { @@ -4274,6 +4398,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4307,6 +4443,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-integer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-integer/-/is-integer-1.0.7.tgz", + "integrity": "sha512-RPQc/s9yBHSvpi+hs9dYiJ2cuFeU6x3TyyIp8O2H6SKEltIvJOzRj9ToyvcStDvPR/pS4rxgr1oBFajQjZ2Szg==", + "license": "WTFPL OR ISC", + "dependencies": { + "is-finite": "^1.0.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4339,6 +4484,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5108,6 +5259,15 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5188,6 +5348,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5821,6 +5988,12 @@ "url": "https://github.com/chalk/ansi-styles?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", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5942,6 +6115,27 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6122,6 +6316,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.6.tgz", + "integrity": "sha512-8zci48uUQyfqynGDSkUMD7FCJB96hwLnlZOXlgs1l3TX+LW27t3psSWKUxC0fxVgA86i8tL4NwGcY1h/6t3ESg==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -6425,6 +6625,21 @@ "node": ">= 0.8" } }, + "node_modules/stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", + "license": "MIT", + "dependencies": { + "emitter-component": "^1.1.1" + } + }, + "node_modules/stream-transform": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-0.1.2.tgz", + "integrity": "sha512-3HXId/0W8sktQnQM6rOZf2LuDDMbakMgAjpViLk758/h0br+iGqZFFfUxxJSqEvGvT742PyFr4v/TBXUtowdCg==", + "license": "BSD-3-Clause" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6454,6 +6669,16 @@ "node": ">=10" } }, + "node_modules/string-to-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", + "integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.1.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7176,6 +7401,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xmldoc": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-0.4.0.tgz", + "integrity": "sha512-rJ/+/UzYCSlFNuAzGuRyYgkH2G5agdX1UQn4+5siYw9pkNC3Hu/grYNDx/dqYLreeSjnY5oKg74CMBKxJHSg6Q==", + "license": "MIT", + "dependencies": { + "sax": "~1.1.1" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7271,9 +7505,9 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@litlfred/fmlrunner-core": "0.1.0", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", + "fhirpath": "^4.6.0", "winston": "^3.11.0" }, "devDependencies": { @@ -7284,11 +7518,6 @@ "npm": ">=8.0.0" } }, - "packages/fmlrunner-kotlin-core": { - "name": "@litlfred/fmlrunner-core", - "version": "0.1.0", - "license": "MIT" - }, "packages/fmlrunner-mcp": { "version": "0.1.0", "license": "MIT", diff --git a/packages/fmlrunner-mcp/src/index.ts b/packages/fmlrunner-mcp/src/index.ts index 66db016..6d676e1 100644 --- a/packages/fmlrunner-mcp/src/index.ts +++ b/packages/fmlrunner-mcp/src/index.ts @@ -85,21 +85,6 @@ export class FmlRunnerMcp { }; this.ajv.addSchema(fmlCompilationInputSchema, 'fml-compilation-input'); - // FML Syntax Validation Input Schema - const fmlSyntaxValidationInputSchema = { - type: 'object', - properties: { - fmlContent: { - type: 'string', - minLength: 1, - description: 'FHIR Mapping Language (FML) content to validate' - } - }, - required: ['fmlContent'], - additionalProperties: false - }; - this.ajv.addSchema(fmlSyntaxValidationInputSchema, 'fml-syntax-validation-input'); - // StructureMap Execution Input Schema const structureMapExecutionInputSchema = { type: 'object', @@ -209,20 +194,6 @@ export class FmlRunnerMcp { required: ['fmlContent'] } }, - { - name: 'validate-fml-syntax', - description: 'Validate FHIR Mapping Language (FML) syntax without compilation', - inputSchema: { - type: 'object', - properties: { - fmlContent: { - type: 'string', - description: 'FML content to validate' - } - }, - required: ['fmlContent'] - } - }, { name: 'execute-structuremap', description: 'Execute a StructureMap transformation on input data', @@ -357,9 +328,6 @@ export class FmlRunnerMcp { case 'compile-fml': return await this.handleCompileFml(args); - case 'validate-fml-syntax': - return await this.handleValidateFmlSyntax(args); - case 'execute-structuremap': return await this.handleExecuteStructureMap(args); @@ -422,29 +390,6 @@ export class FmlRunnerMcp { }; } - private async handleValidateFmlSyntax(args: any): Promise { - // Validate input - const validate = this.ajv.getSchema('fml-syntax-validation-input'); - if (!validate || !validate(args)) { - throw new Error(`Invalid input: ${validate?.errors?.map(e => e.message).join(', ')}`); - } - - const result = this.fmlRunner.validateFmlSyntax(args.fmlContent); - - return { - content: [ - { - type: 'text', - text: JSON.stringify({ - valid: result.valid, - errors: result.errors, - warnings: result.warnings || [] - }, null, 2) - } - ] - }; - } - private async handleExecuteStructureMap(args: any): Promise { // Validate input const validate = this.ajv.getSchema('structuremap-execution-input'); diff --git a/packages/fmlrunner-rest/src/api.ts b/packages/fmlrunner-rest/src/api.ts index d0f8335..59e5146 100644 --- a/packages/fmlrunner-rest/src/api.ts +++ b/packages/fmlrunner-rest/src/api.ts @@ -87,9 +87,8 @@ export class FmlRunnerApi { // Enhanced execution with validation apiRouter.post('/execute-with-validation', this.executeWithValidation.bind(this)); - // Validation endpoints + // Validation endpoint apiRouter.post('/validate', this.validateResource.bind(this)); - apiRouter.post('/fml/validate-syntax', this.validateFmlSyntax.bind(this)); // Health check endpoint apiRouter.get('/health', this.healthCheck.bind(this)); @@ -744,78 +743,6 @@ export class FmlRunnerApi { } } - /** - * Validate FML syntax - */ - private async validateFmlSyntax(req: Request, res: Response): Promise { - try { - const { fmlContent } = req.body; - - if (fmlContent === undefined || fmlContent === null) { - res.status(400).json({ - resourceType: 'OperationOutcome', - issue: [{ - severity: 'error', - code: 'invalid', - diagnostics: 'fmlContent is required' - }] - }); - return; - } - - const result = this.fmlRunner.validateFmlSyntax(fmlContent); - - if (result.valid) { - res.json({ - resourceType: 'OperationOutcome', - issue: [ - { - severity: 'information', - code: 'informational', - diagnostics: 'FML syntax is valid' - }, - // Include warnings if any - ...(result.warnings || []).map(warning => ({ - severity: 'warning' as const, - code: 'informational' as const, - diagnostics: warning.message, - location: [`line ${warning.line}, column ${warning.column}`] - })) - ] - }); - } else { - res.status(400).json({ - resourceType: 'OperationOutcome', - issue: [ - // Add errors - ...result.errors.map(error => ({ - severity: 'error' as const, - code: 'invalid' as const, - diagnostics: error.message, - location: [`line ${error.line}, column ${error.column}`] - })), - // Add warnings if any - ...(result.warnings || []).map(warning => ({ - severity: 'warning' as const, - code: 'informational' as const, - diagnostics: warning.message, - location: [`line ${warning.line}, column ${warning.column}`] - })) - ] - }); - } - } catch (error) { - res.status(500).json({ - resourceType: 'OperationOutcome', - issue: [{ - severity: 'error', - code: 'exception', - diagnostics: error instanceof Error ? error.message : 'Unknown error' - }] - }); - } - } - /** * Health check endpoint */ diff --git a/packages/fmlrunner-rest/tests/fml-syntax-validation-api.test.ts b/packages/fmlrunner-rest/tests/fml-syntax-validation-api.test.ts deleted file mode 100644 index 3da4194..0000000 --- a/packages/fmlrunner-rest/tests/fml-syntax-validation-api.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import request from 'supertest'; -import { FmlRunnerApi } from '../src/api'; -import { FmlRunner } from 'fmlrunner'; - -describe('FML Syntax Validation API', () => { - let app: any; - let fmlRunner: FmlRunner; - - beforeEach(() => { - fmlRunner = new FmlRunner({ logLevel: 'warn' }); - const api = new FmlRunnerApi(fmlRunner); - app = api.getApp(); - }); - - describe('POST /api/v1/fml/validate-syntax', () => { - it('should validate valid FML syntax', async () => { - const validFml = ` - map "http://example.org/fhir/StructureMap/PatientTransform" = "PatientTransform" - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - group Patient(source src : Patient, target tgt) { - src.name -> tgt.name; - } - `; - - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: validFml }) - .expect(200); - - expect(response.body.resourceType).toBe('OperationOutcome'); - expect(response.body.issue).toBeDefined(); - expect(response.body.issue[0].severity).toBe('information'); - expect(response.body.issue[0].diagnostics).toContain('valid'); - }); - - it('should return validation errors for invalid FML', async () => { - const invalidFml = ` - // Missing map declaration - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - group Test { - `; - - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: invalidFml }) - .expect(400); - - expect(response.body.resourceType).toBe('OperationOutcome'); - expect(response.body.issue).toBeDefined(); - expect(response.body.issue.some((issue: any) => issue.severity === 'error')).toBe(true); - }); - - it('should return 400 for missing fmlContent', async () => { - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({}) - .expect(400); - - expect(response.body.resourceType).toBe('OperationOutcome'); - expect(response.body.issue[0].severity).toBe('error'); - expect(response.body.issue[0].diagnostics).toContain('fmlContent is required'); - }); - - it('should handle empty FML content', async () => { - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: '' }) - .expect(400); - - expect(response.body.resourceType).toBe('OperationOutcome'); - expect(response.body.issue[0].severity).toBe('error'); - expect(response.body.issue[0].diagnostics).toContain('empty'); - }); - - it('should include line and column information in errors', async () => { - const invalidFml = `map "test" = "Test" -group Test(source src { - // Missing closing paren -}`; - - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: invalidFml }) - .expect(400); - - expect(response.body.resourceType).toBe('OperationOutcome'); - - const errorIssues = response.body.issue.filter((issue: any) => issue.severity === 'error'); - expect(errorIssues.length).toBeGreaterThan(0); - - errorIssues.forEach((issue: any) => { - expect(issue.location).toBeDefined(); - expect(issue.location[0]).toMatch(/line \d+, column \d+/); - }); - }); - - it('should handle warnings appropriately', async () => { - const fmlWithWarnings = ` - map "http://example.org/fhir/StructureMap/Empty" = "Empty" - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - `; - - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: fmlWithWarnings }); - - expect(response.body.resourceType).toBe('OperationOutcome'); - - // May have warnings but should succeed if no errors - if (response.status === 200) { - const warningIssues = response.body.issue.filter((issue: any) => issue.severity === 'warning'); - expect(warningIssues.length).toBeGreaterThanOrEqual(0); - } - }); - - it('should handle malformed JSON gracefully', async () => { - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send('invalid json') - .expect(400); - - // Should handle malformed request bodies - expect(response.body).toBeDefined(); - }); - - it('should validate FML with complex structures', async () => { - const complexFml = ` - map "http://example.org/fhir/StructureMap/ComplexTransform" = "ComplexTransform" - - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - uses "http://example.org/StructureDefinition/MyPatient" alias MyPatient as target - - group Patient(source src : Patient, target tgt : MyPatient) { - src.id -> tgt.id; - src.name as srcName -> tgt.name as tgtName then { - srcName.family -> tgtName.family; - srcName.given -> tgtName.given; - }; - src.telecom where use = 'phone' -> tgt.phone; - src.address where use = 'home' -> tgt.homeAddress; - } - `; - - const response = await request(app) - .post('/api/v1/fml/validate-syntax') - .send({ fmlContent: complexFml }); - - expect(response.body.resourceType).toBe('OperationOutcome'); - - // Complex but valid FML should pass validation - if (response.status === 200) { - expect(response.body.issue[0].severity).toBe('information'); - } - }); - }); -}); \ No newline at end of file diff --git a/packages/fmlrunner/src/index.ts b/packages/fmlrunner/src/index.ts index 195b94f..ff3abd4 100644 --- a/packages/fmlrunner/src/index.ts +++ b/packages/fmlrunner/src/index.ts @@ -11,7 +11,6 @@ import { SchemaValidator } from './lib/schema-validator'; import { StructureMap, FmlCompilationResult, - FmlSyntaxValidationResult, ExecutionResult, EnhancedExecutionResult, ExecutionOptions, @@ -122,25 +121,6 @@ export class FmlRunner { return result; } - /** - * Validate FML syntax without compiling to StructureMap - */ - validateFmlSyntax(fmlContent: string): FmlSyntaxValidationResult { - this.logger.debug('Validating FML syntax', { contentLength: fmlContent.length }); - - // For syntax validation, we skip the input schema validation - // since we want to catch syntax errors that would fail schema validation - const result = this.compiler.validateSyntax(fmlContent); - - this.logger.info('FML syntax validation completed', { - valid: result.valid, - errorCount: result.errors.length, - warningCount: result.warnings?.length || 0 - }); - - return result; - } - /** * Execute StructureMap on input content with validation */ diff --git a/packages/fmlrunner/src/lib/fml-compiler.ts b/packages/fmlrunner/src/lib/fml-compiler.ts index a3ef150..38d75dd 100644 --- a/packages/fmlrunner/src/lib/fml-compiler.ts +++ b/packages/fmlrunner/src/lib/fml-compiler.ts @@ -1,4 +1,4 @@ -import { StructureMap, FmlCompilationResult, FmlSyntaxValidationResult, FmlSyntaxError, FmlSyntaxWarning, StructureMapGroup, StructureMapGroupInput, StructureMapGroupRule, StructureMapGroupRuleSource, StructureMapGroupRuleTarget } from '../types'; +import { StructureMap, FmlCompilationResult, StructureMapGroup, StructureMapGroupInput, StructureMapGroupRule, StructureMapGroupRuleSource, StructureMapGroupRuleTarget } from '../types'; import { Logger } from './logger'; /** @@ -727,211 +727,6 @@ export class FmlCompiler { } } - /** - * Validate FML syntax without compiling to StructureMap - * @param fmlContent The FML content to validate - * @returns Syntax validation result with detailed error information - */ - validateSyntax(fmlContent: string): FmlSyntaxValidationResult { - const errors: FmlSyntaxError[] = []; - const warnings: FmlSyntaxWarning[] = []; - - try { - // Basic validation - if (!fmlContent || fmlContent.trim().length === 0) { - errors.push({ - line: 1, - column: 1, - message: 'FML content cannot be empty', - severity: 'error', - code: 'EMPTY_CONTENT' - }); - return { valid: false, errors, warnings }; - } - - // Check if content has a map declaration (skip comments and whitespace) - const trimmedContent = fmlContent.trim(); - // Remove leading comments to find the actual start - const contentWithoutLeadingComments = trimmedContent - .replace(/^(?:\/\/.*?\n|\/\*[\s\S]*?\*\/|\s)*/gm, '') - .trim(); - - if (!contentWithoutLeadingComments.toLowerCase().startsWith('map')) { - errors.push({ - line: 1, - column: 1, - message: 'FML content must start with a map declaration', - severity: 'error', - code: 'MISSING_MAP_DECLARATION' - }); - return { valid: false, errors, warnings }; - } - - // Attempt tokenization to catch syntax errors - const tokenizer = new FmlTokenizer(fmlContent); - let tokens: Token[]; - try { - tokens = tokenizer.tokenize(); - } catch (tokenError) { - const error = tokenError as Error; - const match = error.message.match(/line (\d+), column (\d+)/); - if (match) { - errors.push({ - line: parseInt(match[1]), - column: parseInt(match[2]), - message: error.message, - severity: 'error', - code: 'TOKENIZATION_ERROR' - }); - } else { - errors.push({ - line: 1, - column: 1, - message: `Tokenization error: ${error.message}`, - severity: 'error', - code: 'TOKENIZATION_ERROR' - }); - } - return { valid: false, errors, warnings }; - } - - // Basic syntax checks on tokens - this.validateTokenStructure(tokens, errors, warnings); - - // Attempt parsing to catch structural errors - try { - const parser = new FmlParser(tokens); - parser.parse(); - } catch (parseError) { - const error = parseError as Error; - const match = error.message.match(/line (\d+), column (\d+)/); - if (match) { - errors.push({ - line: parseInt(match[1]), - column: parseInt(match[2]), - message: error.message, - severity: 'error', - code: 'PARSE_ERROR' - }); - } else { - errors.push({ - line: 1, - column: 1, - message: `Parse error: ${error.message}`, - severity: 'error', - code: 'PARSE_ERROR' - }); - } - return { valid: false, errors, warnings }; - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - - } catch (error) { - errors.push({ - line: 1, - column: 1, - message: error instanceof Error ? error.message : 'Unknown validation error', - severity: 'error', - code: 'VALIDATION_ERROR' - }); - return { valid: false, errors, warnings }; - } - } - - /** - * Validate token structure and add warnings for common issues - */ - private validateTokenStructure(tokens: Token[], errors: FmlSyntaxError[], warnings: FmlSyntaxWarning[]): void { - let hasMapKeyword = false; - let hasGroupKeyword = false; - let braceCount = 0; - let parenCount = 0; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - // Check for required keywords - if (token.type === TokenType.MAP) { - hasMapKeyword = true; - } - if (token.type === TokenType.GROUP) { - hasGroupKeyword = true; - } - - // Track brace/paren balance - if (token.type === TokenType.LBRACE) braceCount++; - if (token.type === TokenType.RBRACE) braceCount--; - if (token.type === TokenType.LPAREN) parenCount++; - if (token.type === TokenType.RPAREN) parenCount--; - - // Check for unbalanced brackets - if (braceCount < 0) { - errors.push({ - line: token.line, - column: token.column, - message: 'Unmatched closing brace', - severity: 'error', - code: 'UNMATCHED_BRACE' - }); - } - if (parenCount < 0) { - errors.push({ - line: token.line, - column: token.column, - message: 'Unmatched closing parenthesis', - severity: 'error', - code: 'UNMATCHED_PAREN' - }); - } - } - - // Final validation - if (!hasMapKeyword) { - errors.push({ - line: 1, - column: 1, - message: 'Missing required map declaration', - severity: 'error', - code: 'MISSING_MAP' - }); - } - - if (!hasGroupKeyword) { - warnings.push({ - line: 1, - column: 1, - message: 'No group definitions found', - severity: 'warning', - code: 'NO_GROUPS' - }); - } - - if (braceCount > 0) { - errors.push({ - line: 1, - column: 1, - message: `${braceCount} unclosed brace(s)`, - severity: 'error', - code: 'UNCLOSED_BRACE' - }); - } - - if (parenCount > 0) { - errors.push({ - line: 1, - column: 1, - message: `${parenCount} unclosed parenthesis(es)`, - severity: 'error', - code: 'UNCLOSED_PAREN' - }); - } - } - /** * Legacy method for backwards compatibility - now uses the new parser * @deprecated Use compile() method instead diff --git a/packages/fmlrunner/src/types/index.ts b/packages/fmlrunner/src/types/index.ts index 2f870cc..d03bbf7 100644 --- a/packages/fmlrunner/src/types/index.ts +++ b/packages/fmlrunner/src/types/index.ts @@ -63,37 +63,6 @@ export interface FmlCompilationResult { errors?: string[]; } -/** - * FML syntax validation result - */ -export interface FmlSyntaxValidationResult { - valid: boolean; - errors: FmlSyntaxError[]; - warnings?: FmlSyntaxWarning[]; -} - -/** - * FML syntax error - */ -export interface FmlSyntaxError { - line: number; - column: number; - message: string; - severity: 'error'; - code?: string; -} - -/** - * FML syntax warning - */ -export interface FmlSyntaxWarning { - line: number; - column: number; - message: string; - severity: 'warning'; - code?: string; -} - /** * StructureMap execution result */ diff --git a/packages/fmlrunner/tests/fml-syntax-validation.test.ts b/packages/fmlrunner/tests/fml-syntax-validation.test.ts deleted file mode 100644 index d0e77f4..0000000 --- a/packages/fmlrunner/tests/fml-syntax-validation.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { FmlRunner } from '../src/index'; -import { Logger } from '../src/lib/logger'; - -describe('FML Syntax Validation', () => { - let fmlRunner: FmlRunner; - - beforeEach(() => { - fmlRunner = new FmlRunner({ - baseUrl: './tests/test-data', - logLevel: 'warn' // Reduce log noise during tests - }); - }); - - describe('validateFmlSyntax', () => { - it('should validate valid FML content', () => { - const validFml = ` - map "http://example.org/fhir/StructureMap/PatientTransform" = "PatientTransform" - - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - uses "http://example.org/StructureDefinition/MyPatient" alias MyPatient as target - - group Patient(source src : Patient, target tgt : MyPatient) { - src.name -> tgt.name; - src.gender -> tgt.gender; - } - `; - - const result = fmlRunner.validateFmlSyntax(validFml); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should detect empty content', () => { - const result = fmlRunner.validateFmlSyntax(''); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].message).toContain('empty'); - expect(result.errors[0].code).toBe('EMPTY_CONTENT'); - }); - - it('should detect missing map declaration', () => { - const invalidFml = ` - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - group Patient(source src : Patient, target tgt : MyPatient) { - src.name -> tgt.name; - } - `; - - const result = fmlRunner.validateFmlSyntax(invalidFml); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MISSING_MAP_DECLARATION')).toBe(true); - }); - - it('should detect unmatched braces', () => { - const invalidFml = ` - map "http://example.org/fhir/StructureMap/Test" = "Test" - group Test(source src, target tgt) { - src.name -> tgt.name; - // Missing closing brace - `; - - const result = fmlRunner.validateFmlSyntax(invalidFml); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'UNCLOSED_BRACE')).toBe(true); - }); - - it('should detect unmatched parentheses', () => { - const invalidFml = ` - map "http://example.org/fhir/StructureMap/Test" = "Test" - group Test(source src, target tgt { - src.name -> tgt.name; - } - `; - - const result = fmlRunner.validateFmlSyntax(invalidFml); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'UNCLOSED_PAREN')).toBe(true); - }); - - it('should provide line and column information for errors', () => { - const invalidFml = `map "test" = "Test" -group Test(source src, target tgt) { - src.name -> tgt.name - // Missing semicolon above -}`; - - const result = fmlRunner.validateFmlSyntax(invalidFml); - - // Should have errors with line/column info - result.errors.forEach(error => { - expect(error.line).toBeGreaterThan(0); - expect(error.column).toBeGreaterThan(0); - expect(error.severity).toBe('error'); - }); - }); - - it('should include warnings for missing groups', () => { - const fmlWithoutGroups = ` - map "http://example.org/fhir/StructureMap/Empty" = "Empty" - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - `; - - const result = fmlRunner.validateFmlSyntax(fmlWithoutGroups); - - // May be valid but should have warnings - if (result.warnings) { - expect(result.warnings.some(w => w.code === 'NO_GROUPS')).toBe(true); - } - }); - - it('should handle syntax errors gracefully', () => { - const malformedFml = ` - map "test" = "Test" - invalid syntax here @#$% - group Test { - `; - - const result = fmlRunner.validateFmlSyntax(malformedFml); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - - // All errors should have proper structure - result.errors.forEach(error => { - expect(error).toHaveProperty('line'); - expect(error).toHaveProperty('column'); - expect(error).toHaveProperty('message'); - expect(error).toHaveProperty('severity'); - expect(error.severity).toBe('error'); - }); - }); - - it('should validate minimal valid FML', () => { - const minimalFml = 'map "test" = "Test"'; - - const result = fmlRunner.validateFmlSyntax(minimalFml); - - // Should be valid (may have warnings about missing groups) - expect(result.valid || result.errors.every(e => e.severity !== 'error')).toBe(true); - }); - - it('should detect content not starting with map', () => { - const invalidStart = ` - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - map "http://example.org/fhir/StructureMap/Test" = "Test" - `; - - const result = fmlRunner.validateFmlSyntax(invalidStart); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MISSING_MAP_DECLARATION')).toBe(true); - }); - - it('should handle whitespace and comments appropriately', () => { - const fmlWithComments = ` - // This is a comment - map "http://example.org/fhir/StructureMap/Test" = "Test" - - /* Multi-line - comment */ - uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as source - - group Test(source src : Patient, target tgt) { - // Inline comment - src.name -> tgt.name; // Another comment - } - `; - - const result = fmlRunner.validateFmlSyntax(fmlWithComments); - - // Comments should not cause validation errors - expect(result.valid).toBe(true); - }); - }); -}); \ No newline at end of file diff --git a/packages/fmlrunner/tsconfig.json b/packages/fmlrunner/tsconfig.json index f0d657d..aba4d5c 100644 --- a/packages/fmlrunner/tsconfig.json +++ b/packages/fmlrunner/tsconfig.json @@ -14,5 +14,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests", "src/index-with-kotlin.ts", "src/lib/kotlin-bridge.ts"] + "exclude": ["node_modules", "dist", "tests"] } \ No newline at end of file