Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 13 additions & 242 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions packages/fmlrunner-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ export class FmlRunnerMcp {
required: ['fmlContent']
}
},
{
name: 'validate-fml-syntax',
description: 'Validate FHIR Mapping Language (FML) syntax without compilation - provides detailed error reporting',
inputSchema: {
type: 'object',
properties: {
fmlContent: {
type: 'string',
description: 'FML content to validate syntax (must start with map declaration)'
}
},
required: ['fmlContent']
}
},
{
name: 'execute-structuremap',
description: 'Execute a StructureMap transformation on input data',
Expand Down Expand Up @@ -328,6 +342,9 @@ 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);

Expand Down Expand Up @@ -390,6 +407,30 @@ export class FmlRunnerMcp {
};
}

private async handleValidateFmlSyntax(args: any): Promise<CallToolResult> {
// Validate input using the same schema as compile-fml
const validate = this.ajv.getSchema('fml-compilation-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({
success: result.success,
isValid: result.isValid,
errors: result.errors || [],
warnings: result.warnings || []
}, null, 2)
}
]
};
}

private async handleExecuteStructureMap(args: any): Promise<CallToolResult> {
// Validate input
const validate = this.ajv.getSchema('structuremap-execution-input');
Expand Down
38 changes: 38 additions & 0 deletions packages/fmlrunner-rest/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class FmlRunnerApi {

// Legacy endpoints for backward compatibility
apiRouter.post('/compile', this.compileFml.bind(this));
apiRouter.post('/validate-syntax', this.validateFmlSyntax.bind(this));
apiRouter.post('/execute', this.executeStructureMap.bind(this));
apiRouter.get('/structuremap/:reference', this.getStructureMap.bind(this));

Expand Down Expand Up @@ -129,6 +130,43 @@ export class FmlRunnerApi {
}
}

/**
* Validate FML syntax without compilation
*/
private async validateFmlSyntax(req: Request, res: Response): Promise<void> {
try {
const { fmlContent } = req.body;

if (!fmlContent) {
res.status(400).json({
error: 'fmlContent is required',
details: 'Request body must include fmlContent property'
});
return;
}

const result = this.fmlRunner.validateFmlSyntax(fmlContent);

if (result.success) {
res.json({
isValid: result.isValid,
errors: result.errors || [],
warnings: result.warnings || []
});
} else {
res.status(500).json({
error: 'FML syntax validation failed',
details: 'Internal validation error occurred'
});
}
} catch (error) {
res.status(500).json({
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}

/**
* Execute StructureMap transformation
*/
Expand Down
116 changes: 116 additions & 0 deletions packages/fmlrunner-rest/tests/syntax-validation-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import request from 'supertest';
import { FmlRunnerApi } from '../src/api';
import { FmlRunner } from 'fmlrunner';

describe('FML Syntax Validation API', () => {
let app: any;
let fmlRunner: FmlRunner;

beforeAll(() => {
fmlRunner = new FmlRunner({ baseUrl: './tests/test-data' });
const api = new FmlRunnerApi(fmlRunner);
app = api.getApp();
});

describe('POST /api/v1/validate-syntax', () => {
it('should validate valid FML content', async () => {
const validFmlContent = `
map "http://example.org/tutorial" = tutorial

group tutorial(source src : TutorialLeft, target tgt : TutorialRight) {
src.a as a -> tgt.a = a;
}
`;

const response = await request(app)
.post('/api/v1/validate-syntax')
.send({ fmlContent: validFmlContent })
.expect(200);

expect(response.body.isValid).toBe(true);
expect(response.body.errors).toEqual([]);
expect(response.body.warnings).toEqual([]);
});

it('should detect syntax errors in FML content', async () => {
const invalidFmlContent = `
invalid "http://example.org/tutorial" = tutorial

group tutorial(source src : TutorialLeft, target tgt : TutorialRight) {
src.a as a -> tgt.a = a;
}
`;

const response = await request(app)
.post('/api/v1/validate-syntax')
.send({ fmlContent: invalidFmlContent })
.expect(200);

expect(response.body.isValid).toBe(false);
expect(response.body.errors).toBeDefined();
expect(response.body.errors.length).toBeGreaterThan(0);
expect(response.body.errors.some((e: any) => e.code === 'MISSING_MAP_KEYWORD')).toBe(true);
});

it('should return 400 for missing fmlContent', async () => {
const response = await request(app)
.post('/api/v1/validate-syntax')
.send({})
.expect(400);

expect(response.body.error).toBe('fmlContent is required');
});

it('should return 400 for empty fmlContent', async () => {
const response = await request(app)
.post('/api/v1/validate-syntax')
.send({ fmlContent: '' })
.expect(200);

expect(response.body.isValid).toBe(false);
expect(response.body.errors).toBeDefined();
expect(response.body.errors.some((e: any) => e.code === 'EMPTY_CONTENT')).toBe(true);
});

it('should detect unclosed braces', async () => {
const invalidFmlContent = `
map "http://example.org/tutorial" = tutorial

group tutorial(source src : TutorialLeft, target tgt : TutorialRight) {
src.a as a -> tgt.a = a;

`;

const response = await request(app)
.post('/api/v1/validate-syntax')
.send({ fmlContent: invalidFmlContent })
.expect(200);

expect(response.body.isValid).toBe(false);
expect(response.body.errors).toBeDefined();
expect(response.body.errors.some((e: any) => e.code === 'UNCLOSED_BRACE')).toBe(true);
});

it('should provide detailed error information', async () => {
const invalidFmlContent = `map "test" = test

group test(source src, target tgt) {
src.a -> tgt.b;
}}`;

const response = await request(app)
.post('/api/v1/validate-syntax')
.send({ fmlContent: invalidFmlContent })
.expect(200);

expect(response.body.isValid).toBe(false);
expect(response.body.errors).toBeDefined();

const unMatchedError = response.body.errors.find((e: any) => e.code === 'UNMATCHED_BRACE');
expect(unMatchedError).toBeDefined();
expect(unMatchedError.line).toBeDefined();
expect(unMatchedError.column).toBeDefined();
expect(unMatchedError.severity).toBe('error');
});
});
});
20 changes: 20 additions & 0 deletions packages/fmlrunner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SchemaValidator } from './lib/schema-validator';
import {
StructureMap,
FmlCompilationResult,
FmlSyntaxValidationResult,
ExecutionResult,
EnhancedExecutionResult,
ExecutionOptions,
Expand Down Expand Up @@ -121,6 +122,25 @@ export class FmlRunner {
return result;
}

/**
* Validate FML syntax without full compilation
* Provides detailed error reporting for syntax validation
*/
validateFmlSyntax(fmlContent: string): FmlSyntaxValidationResult {
this.logger.debug('Validating FML syntax', { contentLength: fmlContent?.length || 0 });

const result = this.compiler.validateSyntax(fmlContent);

this.logger.info('FML syntax validation completed', {
success: result.success,
isValid: result.isValid,
errorCount: result.errors?.length || 0,
warningCount: result.warnings?.length || 0
});

return result;
}

/**
* Execute StructureMap on input content with validation
*/
Expand Down
Loading
Loading