From f5f952affb00ece90290ce522861a224cdf86c73 Mon Sep 17 00:00:00 2001 From: Joshua Reynolds <48231325+jreyno77@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:49:32 -0600 Subject: [PATCH 1/4] Initial Refactor including buildParameters and initializeContext (#46) * Initial Refactor including buildParameters and initializeContext * Update initializeState.ts to check for files that endwith .cql not include Co-authored-by: holly-smile <166412459+holly-smile@users.noreply.github.com> * Adding back disable-extensions args to launch.json * rename initializeState to normalizedCqlExecution * Ensure readability of the executeCql signature Co-authored-by: holly-smile <166412459+holly-smile@users.noreply.github.com> --------- Co-authored-by: holly-smile <166412459+holly-smile@users.noreply.github.com> --- src/buildParameters.ts | 170 ++++++++++++++++++++++++++++ src/cqlLanguageClient.ts | 4 +- src/executeCql.ts | 214 ++++++----------------------------- src/normalizeCqlExecution.ts | 19 ++++ 4 files changed, 225 insertions(+), 182 deletions(-) create mode 100644 src/buildParameters.ts create mode 100644 src/normalizeCqlExecution.ts diff --git a/src/buildParameters.ts b/src/buildParameters.ts new file mode 100644 index 0000000..cda4eee --- /dev/null +++ b/src/buildParameters.ts @@ -0,0 +1,170 @@ +import { glob } from 'glob'; +import { Uri, window, workspace } from 'vscode'; +import { Utils } from 'vscode-uri'; + +import * as fs from 'fs'; +import * as fse from 'fs-extra'; +import path from 'path'; + +export type EvaluationParameters = { + operationArgs: string[] | undefined, + outputPath: Uri | undefined, + testPath: Uri | undefined +} + +// Should be working with normalized data +export function buildParameters(uri: Uri): EvaluationParameters { + if (!fs.existsSync(uri.fsPath)) { + window.showInformationMessage('No library content found. Please save before executing.'); + return {operationArgs: undefined, outputPath: undefined, testPath: undefined}; + } + + const libraryDirectory = Utils.dirname(uri); + const libraryName = Utils.basename(uri).replace('.cql', '').split('-')[0]; + const projectPath = workspace.getWorkspaceFolder(uri)!.uri; + + // todo: make this a setting + let terminologyPath: Uri = Utils.resolvePath(projectPath, 'input', 'vocabulary', 'valueset'); + + let fhirVersion = getFhirVersion(); + if (!fhirVersion) { + fhirVersion = 'R4'; + window.showInformationMessage('Unable to determine version of FHIR used. Defaulting to R4.'); + } + + const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); + const measurementPeriod = ''; + const testPath = Utils.resolvePath(projectPath, 'input', 'tests'); + const resultPath = Utils.resolvePath(testPath, 'results'); + const outputPath = Utils.resolvePath(resultPath, `${libraryName}.txt`); + + fse.ensureFileSync(outputPath.fsPath); + + var testCasesArgs: string[] = []; + var testPaths = getTestPaths(testPath, libraryName); + + // We didn't find any test cases, so we'll just execute an empty one + if (testPaths.length === 0) { + testPaths.push({ name: null, path: null }); + } + + for (var p of testPaths) { + testCasesArgs.push( + ...getExecArgs( + libraryDirectory, + libraryName, + p.path, + terminologyPath, + p.name, + measurementPeriod, + ), + ); + } + + let operationArgs = getCqlCommandArgs(fhirVersion, optionsPath); + operationArgs.push(...testCasesArgs); + let evaluationParams: EvaluationParameters = { + operationArgs, + outputPath, + testPath + } + return evaluationParams; +} + +function getFhirVersion(): string | null { + const fhirVersionRegex = /using (FHIR|"FHIR") version '(\d(.|\d)*)'/; + const matches = window.activeTextEditor!.document.getText().match(fhirVersionRegex); + if (matches && matches.length > 2) { + const version = matches[2]; + if (version.startsWith('2')) { + return 'DSTU2'; + } else if (version.startsWith('3')) { + return 'DSTU3'; + } else if (version.startsWith('4')) { + return 'R4'; + } else if (version.startsWith('5')) { + return 'R5'; + } + } + + return null; +} + +interface TestCase { + name: string | null; + path: Uri | null; +} + +/** + * Get the test cases to execute + * @param testPath the root path to look for test cases + * @returns a list of test cases to execute + */ +function getTestPaths(testPath: Uri, libraryName: string): TestCase[] { + if (!fs.existsSync(testPath.fsPath)) { + return []; + } + + let testCases: TestCase[] = []; + let directories = glob + .sync(testPath.fsPath + `/**/${libraryName}`) + .filter(d => fs.statSync(d).isDirectory()); + for (var dir of directories) { + let cases = fs.readdirSync(dir).filter(d => fs.statSync(path.join(dir, d)).isDirectory()); + for (var c of cases) { + testCases.push({ name: c, path: Uri.file(path.join(dir, c)) }); + } + } + + return testCases; +} + +function getCqlCommandArgs(fhirVersion: string, optionsPath: Uri): string[] { + const args = ['cql']; + + args.push(`-fv=${fhirVersion}`); + + if (optionsPath && fs.existsSync(optionsPath.fsPath)) { + args.push(`-op=${optionsPath}`); + } + + return args; +} + +function getExecArgs( + libraryDirectory: Uri, + libraryName: string, + modelPath: Uri | null, + terminologyPath: Uri | null, + contextValue: string | null, + measurementPeriod: string, +): string[] { + // TODO: One day we might support other models and contexts + const modelType = 'FHIR'; + const contextType = 'Patient'; + + let args: string[] = []; + args.push(`-ln=${libraryName}`); + args.push(`-lu=${libraryDirectory}`); + + if (modelPath) { + args.push(`-m=${modelType}`); + args.push(`-mu=${modelPath}`); + } + + if (terminologyPath) { + args.push(`-t=${terminologyPath}`); + } + + if (contextValue) { + args.push(`-c=${contextType}`); + args.push(`-cv=${contextValue}`); + } + + if (measurementPeriod && measurementPeriod !== '') { + args.push(`-p=${libraryName}."Measurement Period"`); + args.push(`-pv=${measurementPeriod}`); + } + + return args; +} diff --git a/src/cqlLanguageClient.ts b/src/cqlLanguageClient.ts index 7f25022..a757ec4 100644 --- a/src/cqlLanguageClient.ts +++ b/src/cqlLanguageClient.ts @@ -7,10 +7,10 @@ import { } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; import { Commands } from './commands'; -import { executeCQLFile } from './executeCql'; import { ClientStatus } from './extension.api'; import { prepareExecutable } from './languageServerStarter'; import { logger } from './log'; +import { normalizeCqlExecution } from './normalizeCqlExecution'; import { ActionableNotification, ExecuteClientCommandRequest, @@ -142,7 +142,7 @@ export class CqlLanguageClient { context.subscriptions.push( commands.registerCommand(Commands.EXECUTE_CQL_COMMAND, async (uri: Uri) => { - await executeCQLFile(uri); + await normalizeCqlExecution(uri); }), ); } diff --git a/src/executeCql.ts b/src/executeCql.ts index 979b46f..64976f1 100644 --- a/src/executeCql.ts +++ b/src/executeCql.ts @@ -1,150 +1,54 @@ -import { glob } from 'glob'; import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; -import { Utils } from 'vscode-uri'; import { Commands } from './commands'; import * as fs from 'fs'; -import * as fse from 'fs-extra'; -import path from 'path'; +import { EvaluationParameters } from './buildParameters'; -// NOTE: This is not the intended future state of executing CQL. -// There's a CQL debug server under development that will replace this. -export async function executeCQLFile(uri: Uri): Promise { - if (!fs.existsSync(uri.fsPath)) { - window.showInformationMessage('No library content found. Please save before executing.'); - return; - } - - const libraryDirectory = Utils.dirname(uri); - const libraryName = Utils.basename(uri).replace('.cql', '').split('-')[0]; - const projectPath = workspace.getWorkspaceFolder(uri)!.uri; - - // todo: make this a setting - let terminologyPath: Uri = Utils.resolvePath(projectPath, 'input', 'vocabulary', 'valueset'); - - let fhirVersion = getFhirVersion(); - if (!fhirVersion) { - fhirVersion = 'R4'; - window.showInformationMessage('Unable to determine version of FHIR used. Defaulting to R4.'); - } - - const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); - const measurementPeriod = ''; - const testPath = Utils.resolvePath(projectPath, 'input', 'tests'); - const resultPath = Utils.resolvePath(testPath, 'results'); - const outputPath = Utils.resolvePath(resultPath, `${libraryName}.txt`); - - fse.ensureFileSync(outputPath.fsPath); - - const textDocument = await workspace.openTextDocument(outputPath); - const textEditor = await window.showTextDocument(textDocument); - - var testCasesArgs: string[] = []; - var testPaths = getTestPaths(testPath, libraryName); - - // We didn't find any test cases, so we'll just execute an empty one - if (testPaths.length === 0) { - testPaths.push({ name: null, path: null }); - } +async function insertLineAtEnd(textEditor: TextEditor, text: string) { + const document = textEditor.document; + await textEditor.edit(editBuilder => { + editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); + }); +} - for (var p of testPaths) { - testCasesArgs.push( - ...getExecArgs( - libraryDirectory, - libraryName, - p.path, - terminologyPath, - p.name, - measurementPeriod, - ), - ); +export async function executeCQL({operationArgs, testPath, outputPath}: EvaluationParameters) { + let cqlMessage = ''; + let terminologyMessage = ''; + let testMessage = `Test cases:\n`; + let foundTest = false; + for (let i = 0; i < evaluationParameters.operationArgs?.length!; i++) { + if (evaluationParameters.operationArgs![i].startsWith("-lu=")) { + cqlMessage = `CQL: ${evaluationParameters.operationArgs![i].substring(4)}`; + } else if (evaluationParameters.operationArgs![i].startsWith("-t=")) { + let terminologyUri = Uri.parse(evaluationParameters.operationArgs![i].substring(3)); + terminologyMessage = fs.existsSync(terminologyUri.fsPath) + ? `Terminology: ${evaluationParameters.operationArgs![i].substring(3)}` + : `No terminology found at ${evaluationParameters.operationArgs![i].substring(3)}. Evaluation may fail if terminology is required.`; + } else if (evaluationParameters.operationArgs![i].startsWith("-mu=")) { + foundTest = true; + testMessage += `${evaluationParameters.operationArgs![i].substring(4)}`; + } else if (evaluationParameters.operationArgs![i].startsWith("-cv=")) { + foundTest = true; + testMessage += ` - ${evaluationParameters.operationArgs![i].substring(4)} \n` + } } - const cqlMessage = `CQL: ${libraryDirectory.fsPath}`; - const terminologyMessage = fs.existsSync(terminologyPath.fsPath) - ? `Terminology: ${terminologyPath.fsPath}` - : `No terminology found at ${terminologyPath.fsPath}. Evaluation may fail if terminology is required.`; - - let testMessage = ''; - if (testPaths.length == 1 && testPaths[0].name === null) { - testMessage = `No data found at ${testPath.fsPath}. Evaluation may fail if data is required.`; - } else { - testMessage = `Test cases:\n`; - for (var p of testPaths) { - testMessage += `${p.name} - ${p.path?.fsPath}\n`; - } + if (!foundTest) { + testMessage = `No data found at path ${evaluationParameters.testPath!}. Evaluation may fail if data is required.`; } + const textDocument = await workspace.openTextDocument(evaluationParameters.outputPath!); + const textEditor = await window.showTextDocument(textDocument); + await insertLineAtEnd(textEditor, `${cqlMessage}`); await insertLineAtEnd(textEditor, `${terminologyMessage}`); await insertLineAtEnd(textEditor, `${testMessage}`); - - let operationArgs = getCqlCommandArgs(fhirVersion, optionsPath); - operationArgs.push(...testCasesArgs); - await executeCQL(textEditor, operationArgs); -} - -function getFhirVersion(): string | null { - const fhirVersionRegex = /using (FHIR|"FHIR") version '(\d(.|\d)*)'/; - const matches = window.activeTextEditor!.document.getText().match(fhirVersionRegex); - if (matches && matches.length > 2) { - const version = matches[2]; - if (version.startsWith('2')) { - return 'DSTU2'; - } else if (version.startsWith('3')) { - return 'DSTU3'; - } else if (version.startsWith('4')) { - return 'R4'; - } else if (version.startsWith('5')) { - return 'R5'; - } - } - - return null; -} - -interface TestCase { - name: string | null; - path: Uri | null; -} - -/** - * Get the test cases to execute - * @param testPath the root path to look for test cases - * @returns a list of test cases to execute - */ -function getTestPaths(testPath: Uri, libraryName: string): TestCase[] { - if (!fs.existsSync(testPath.fsPath)) { - return []; - } - - let testCases: TestCase[] = []; - let directories = glob - .sync(testPath.fsPath + `/**/${libraryName}`) - .filter(d => fs.statSync(d).isDirectory()); - for (var dir of directories) { - let cases = fs.readdirSync(dir).filter(d => fs.statSync(path.join(dir, d)).isDirectory()); - for (var c of cases) { - testCases.push({ name: c, path: Uri.file(path.join(dir, c)) }); - } - } - - return testCases; -} - -async function insertLineAtEnd(textEditor: TextEditor, text: string) { - const document = textEditor.document; - await textEditor.edit(editBuilder => { - editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); - }); -} - -async function executeCQL(textEditor: TextEditor, operationArgs: string[]) { + const startExecution = Date.now(); const result: string | undefined = await commands.executeCommand( Commands.EXECUTE_WORKSPACE_COMMAND, Commands.EXECUTE_CQL, - ...operationArgs, + ...evaluationParameters.operationArgs!, ); const endExecution = Date.now(); @@ -154,53 +58,3 @@ async function executeCQL(textEditor: TextEditor, operationArgs: string[]) { `elapsed: ${((endExecution - startExecution) / 1000).toString()} seconds`, ); } - -function getCqlCommandArgs(fhirVersion: string, optionsPath: Uri): string[] { - const args = ['cql']; - - args.push(`-fv=${fhirVersion}`); - - if (optionsPath && fs.existsSync(optionsPath.fsPath)) { - args.push(`-op=${optionsPath}`); - } - - return args; -} - -function getExecArgs( - libraryDirectory: Uri, - libraryName: string, - modelPath: Uri | null, - terminologyPath: Uri | null, - contextValue: string | null, - measurementPeriod: string, -): string[] { - // TODO: One day we might support other models and contexts - const modelType = 'FHIR'; - const contextType = 'Patient'; - - let args: string[] = []; - args.push(`-ln=${libraryName}`); - args.push(`-lu=${libraryDirectory}`); - - if (modelPath) { - args.push(`-m=${modelType}`); - args.push(`-mu=${modelPath}`); - } - - if (terminologyPath) { - args.push(`-t=${terminologyPath}`); - } - - if (contextValue) { - args.push(`-c=${contextType}`); - args.push(`-cv=${contextValue}`); - } - - if (measurementPeriod && measurementPeriod !== '') { - args.push(`-p=${libraryName}."Measurement Period"`); - args.push(`-pv=${measurementPeriod}`); - } - - return args; -} diff --git a/src/normalizeCqlExecution.ts b/src/normalizeCqlExecution.ts new file mode 100644 index 0000000..23b0d93 --- /dev/null +++ b/src/normalizeCqlExecution.ts @@ -0,0 +1,19 @@ +import { Uri, window } from "vscode"; +import { buildParameters } from "./buildParameters"; +import { executeCQL } from "./executeCql"; + +export async function normalizeCqlExecution(uri: Uri) { + + // Needs a distinction between CQL file and single line + const isCqlFile = window.activeTextEditor!.document.fileName.endsWith(".cql"); + + if (isCqlFile) { + // should normalize data + let operationArgs = buildParameters(uri) + executeCQL(operationArgs) + } else { + window.showInformationMessage('As of now we only support Cql File Execution and execution needs to run a .cql file.'); + } + +} + From 88c280eb819d0fbb14fc7b33814918304af0c6b5 Mon Sep 17 00:00:00 2001 From: Joshua Reynolds <48231325+jreyno77@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:52:56 -0600 Subject: [PATCH 2/4] Revert "Initial Refactor including buildParameters and initializeContext (#46)" (#47) This reverts commit f5f952affb00ece90290ce522861a224cdf86c73. --- src/buildParameters.ts | 170 ---------------------------- src/cqlLanguageClient.ts | 4 +- src/executeCql.ts | 214 +++++++++++++++++++++++++++++------ src/normalizeCqlExecution.ts | 19 ---- 4 files changed, 182 insertions(+), 225 deletions(-) delete mode 100644 src/buildParameters.ts delete mode 100644 src/normalizeCqlExecution.ts diff --git a/src/buildParameters.ts b/src/buildParameters.ts deleted file mode 100644 index cda4eee..0000000 --- a/src/buildParameters.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { glob } from 'glob'; -import { Uri, window, workspace } from 'vscode'; -import { Utils } from 'vscode-uri'; - -import * as fs from 'fs'; -import * as fse from 'fs-extra'; -import path from 'path'; - -export type EvaluationParameters = { - operationArgs: string[] | undefined, - outputPath: Uri | undefined, - testPath: Uri | undefined -} - -// Should be working with normalized data -export function buildParameters(uri: Uri): EvaluationParameters { - if (!fs.existsSync(uri.fsPath)) { - window.showInformationMessage('No library content found. Please save before executing.'); - return {operationArgs: undefined, outputPath: undefined, testPath: undefined}; - } - - const libraryDirectory = Utils.dirname(uri); - const libraryName = Utils.basename(uri).replace('.cql', '').split('-')[0]; - const projectPath = workspace.getWorkspaceFolder(uri)!.uri; - - // todo: make this a setting - let terminologyPath: Uri = Utils.resolvePath(projectPath, 'input', 'vocabulary', 'valueset'); - - let fhirVersion = getFhirVersion(); - if (!fhirVersion) { - fhirVersion = 'R4'; - window.showInformationMessage('Unable to determine version of FHIR used. Defaulting to R4.'); - } - - const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); - const measurementPeriod = ''; - const testPath = Utils.resolvePath(projectPath, 'input', 'tests'); - const resultPath = Utils.resolvePath(testPath, 'results'); - const outputPath = Utils.resolvePath(resultPath, `${libraryName}.txt`); - - fse.ensureFileSync(outputPath.fsPath); - - var testCasesArgs: string[] = []; - var testPaths = getTestPaths(testPath, libraryName); - - // We didn't find any test cases, so we'll just execute an empty one - if (testPaths.length === 0) { - testPaths.push({ name: null, path: null }); - } - - for (var p of testPaths) { - testCasesArgs.push( - ...getExecArgs( - libraryDirectory, - libraryName, - p.path, - terminologyPath, - p.name, - measurementPeriod, - ), - ); - } - - let operationArgs = getCqlCommandArgs(fhirVersion, optionsPath); - operationArgs.push(...testCasesArgs); - let evaluationParams: EvaluationParameters = { - operationArgs, - outputPath, - testPath - } - return evaluationParams; -} - -function getFhirVersion(): string | null { - const fhirVersionRegex = /using (FHIR|"FHIR") version '(\d(.|\d)*)'/; - const matches = window.activeTextEditor!.document.getText().match(fhirVersionRegex); - if (matches && matches.length > 2) { - const version = matches[2]; - if (version.startsWith('2')) { - return 'DSTU2'; - } else if (version.startsWith('3')) { - return 'DSTU3'; - } else if (version.startsWith('4')) { - return 'R4'; - } else if (version.startsWith('5')) { - return 'R5'; - } - } - - return null; -} - -interface TestCase { - name: string | null; - path: Uri | null; -} - -/** - * Get the test cases to execute - * @param testPath the root path to look for test cases - * @returns a list of test cases to execute - */ -function getTestPaths(testPath: Uri, libraryName: string): TestCase[] { - if (!fs.existsSync(testPath.fsPath)) { - return []; - } - - let testCases: TestCase[] = []; - let directories = glob - .sync(testPath.fsPath + `/**/${libraryName}`) - .filter(d => fs.statSync(d).isDirectory()); - for (var dir of directories) { - let cases = fs.readdirSync(dir).filter(d => fs.statSync(path.join(dir, d)).isDirectory()); - for (var c of cases) { - testCases.push({ name: c, path: Uri.file(path.join(dir, c)) }); - } - } - - return testCases; -} - -function getCqlCommandArgs(fhirVersion: string, optionsPath: Uri): string[] { - const args = ['cql']; - - args.push(`-fv=${fhirVersion}`); - - if (optionsPath && fs.existsSync(optionsPath.fsPath)) { - args.push(`-op=${optionsPath}`); - } - - return args; -} - -function getExecArgs( - libraryDirectory: Uri, - libraryName: string, - modelPath: Uri | null, - terminologyPath: Uri | null, - contextValue: string | null, - measurementPeriod: string, -): string[] { - // TODO: One day we might support other models and contexts - const modelType = 'FHIR'; - const contextType = 'Patient'; - - let args: string[] = []; - args.push(`-ln=${libraryName}`); - args.push(`-lu=${libraryDirectory}`); - - if (modelPath) { - args.push(`-m=${modelType}`); - args.push(`-mu=${modelPath}`); - } - - if (terminologyPath) { - args.push(`-t=${terminologyPath}`); - } - - if (contextValue) { - args.push(`-c=${contextType}`); - args.push(`-cv=${contextValue}`); - } - - if (measurementPeriod && measurementPeriod !== '') { - args.push(`-p=${libraryName}."Measurement Period"`); - args.push(`-pv=${measurementPeriod}`); - } - - return args; -} diff --git a/src/cqlLanguageClient.ts b/src/cqlLanguageClient.ts index a757ec4..7f25022 100644 --- a/src/cqlLanguageClient.ts +++ b/src/cqlLanguageClient.ts @@ -7,10 +7,10 @@ import { } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; import { Commands } from './commands'; +import { executeCQLFile } from './executeCql'; import { ClientStatus } from './extension.api'; import { prepareExecutable } from './languageServerStarter'; import { logger } from './log'; -import { normalizeCqlExecution } from './normalizeCqlExecution'; import { ActionableNotification, ExecuteClientCommandRequest, @@ -142,7 +142,7 @@ export class CqlLanguageClient { context.subscriptions.push( commands.registerCommand(Commands.EXECUTE_CQL_COMMAND, async (uri: Uri) => { - await normalizeCqlExecution(uri); + await executeCQLFile(uri); }), ); } diff --git a/src/executeCql.ts b/src/executeCql.ts index 64976f1..979b46f 100644 --- a/src/executeCql.ts +++ b/src/executeCql.ts @@ -1,54 +1,150 @@ +import { glob } from 'glob'; import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; +import { Utils } from 'vscode-uri'; import { Commands } from './commands'; import * as fs from 'fs'; -import { EvaluationParameters } from './buildParameters'; +import * as fse from 'fs-extra'; +import path from 'path'; -async function insertLineAtEnd(textEditor: TextEditor, text: string) { - const document = textEditor.document; - await textEditor.edit(editBuilder => { - editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); - }); -} - -export async function executeCQL({operationArgs, testPath, outputPath}: EvaluationParameters) { - let cqlMessage = ''; - let terminologyMessage = ''; - let testMessage = `Test cases:\n`; - let foundTest = false; - for (let i = 0; i < evaluationParameters.operationArgs?.length!; i++) { - if (evaluationParameters.operationArgs![i].startsWith("-lu=")) { - cqlMessage = `CQL: ${evaluationParameters.operationArgs![i].substring(4)}`; - } else if (evaluationParameters.operationArgs![i].startsWith("-t=")) { - let terminologyUri = Uri.parse(evaluationParameters.operationArgs![i].substring(3)); - terminologyMessage = fs.existsSync(terminologyUri.fsPath) - ? `Terminology: ${evaluationParameters.operationArgs![i].substring(3)}` - : `No terminology found at ${evaluationParameters.operationArgs![i].substring(3)}. Evaluation may fail if terminology is required.`; - } else if (evaluationParameters.operationArgs![i].startsWith("-mu=")) { - foundTest = true; - testMessage += `${evaluationParameters.operationArgs![i].substring(4)}`; - } else if (evaluationParameters.operationArgs![i].startsWith("-cv=")) { - foundTest = true; - testMessage += ` - ${evaluationParameters.operationArgs![i].substring(4)} \n` - } +// NOTE: This is not the intended future state of executing CQL. +// There's a CQL debug server under development that will replace this. +export async function executeCQLFile(uri: Uri): Promise { + if (!fs.existsSync(uri.fsPath)) { + window.showInformationMessage('No library content found. Please save before executing.'); + return; } - if (!foundTest) { - testMessage = `No data found at path ${evaluationParameters.testPath!}. Evaluation may fail if data is required.`; + const libraryDirectory = Utils.dirname(uri); + const libraryName = Utils.basename(uri).replace('.cql', '').split('-')[0]; + const projectPath = workspace.getWorkspaceFolder(uri)!.uri; + + // todo: make this a setting + let terminologyPath: Uri = Utils.resolvePath(projectPath, 'input', 'vocabulary', 'valueset'); + + let fhirVersion = getFhirVersion(); + if (!fhirVersion) { + fhirVersion = 'R4'; + window.showInformationMessage('Unable to determine version of FHIR used. Defaulting to R4.'); } - const textDocument = await workspace.openTextDocument(evaluationParameters.outputPath!); + const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); + const measurementPeriod = ''; + const testPath = Utils.resolvePath(projectPath, 'input', 'tests'); + const resultPath = Utils.resolvePath(testPath, 'results'); + const outputPath = Utils.resolvePath(resultPath, `${libraryName}.txt`); + + fse.ensureFileSync(outputPath.fsPath); + + const textDocument = await workspace.openTextDocument(outputPath); const textEditor = await window.showTextDocument(textDocument); - + + var testCasesArgs: string[] = []; + var testPaths = getTestPaths(testPath, libraryName); + + // We didn't find any test cases, so we'll just execute an empty one + if (testPaths.length === 0) { + testPaths.push({ name: null, path: null }); + } + + for (var p of testPaths) { + testCasesArgs.push( + ...getExecArgs( + libraryDirectory, + libraryName, + p.path, + terminologyPath, + p.name, + measurementPeriod, + ), + ); + } + + const cqlMessage = `CQL: ${libraryDirectory.fsPath}`; + const terminologyMessage = fs.existsSync(terminologyPath.fsPath) + ? `Terminology: ${terminologyPath.fsPath}` + : `No terminology found at ${terminologyPath.fsPath}. Evaluation may fail if terminology is required.`; + + let testMessage = ''; + if (testPaths.length == 1 && testPaths[0].name === null) { + testMessage = `No data found at ${testPath.fsPath}. Evaluation may fail if data is required.`; + } else { + testMessage = `Test cases:\n`; + for (var p of testPaths) { + testMessage += `${p.name} - ${p.path?.fsPath}\n`; + } + } + await insertLineAtEnd(textEditor, `${cqlMessage}`); await insertLineAtEnd(textEditor, `${terminologyMessage}`); await insertLineAtEnd(textEditor, `${testMessage}`); - + + let operationArgs = getCqlCommandArgs(fhirVersion, optionsPath); + operationArgs.push(...testCasesArgs); + await executeCQL(textEditor, operationArgs); +} + +function getFhirVersion(): string | null { + const fhirVersionRegex = /using (FHIR|"FHIR") version '(\d(.|\d)*)'/; + const matches = window.activeTextEditor!.document.getText().match(fhirVersionRegex); + if (matches && matches.length > 2) { + const version = matches[2]; + if (version.startsWith('2')) { + return 'DSTU2'; + } else if (version.startsWith('3')) { + return 'DSTU3'; + } else if (version.startsWith('4')) { + return 'R4'; + } else if (version.startsWith('5')) { + return 'R5'; + } + } + + return null; +} + +interface TestCase { + name: string | null; + path: Uri | null; +} + +/** + * Get the test cases to execute + * @param testPath the root path to look for test cases + * @returns a list of test cases to execute + */ +function getTestPaths(testPath: Uri, libraryName: string): TestCase[] { + if (!fs.existsSync(testPath.fsPath)) { + return []; + } + + let testCases: TestCase[] = []; + let directories = glob + .sync(testPath.fsPath + `/**/${libraryName}`) + .filter(d => fs.statSync(d).isDirectory()); + for (var dir of directories) { + let cases = fs.readdirSync(dir).filter(d => fs.statSync(path.join(dir, d)).isDirectory()); + for (var c of cases) { + testCases.push({ name: c, path: Uri.file(path.join(dir, c)) }); + } + } + + return testCases; +} + +async function insertLineAtEnd(textEditor: TextEditor, text: string) { + const document = textEditor.document; + await textEditor.edit(editBuilder => { + editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); + }); +} + +async function executeCQL(textEditor: TextEditor, operationArgs: string[]) { const startExecution = Date.now(); const result: string | undefined = await commands.executeCommand( Commands.EXECUTE_WORKSPACE_COMMAND, Commands.EXECUTE_CQL, - ...evaluationParameters.operationArgs!, + ...operationArgs, ); const endExecution = Date.now(); @@ -58,3 +154,53 @@ export async function executeCQL({operationArgs, testPath, outputPath}: Evaluati `elapsed: ${((endExecution - startExecution) / 1000).toString()} seconds`, ); } + +function getCqlCommandArgs(fhirVersion: string, optionsPath: Uri): string[] { + const args = ['cql']; + + args.push(`-fv=${fhirVersion}`); + + if (optionsPath && fs.existsSync(optionsPath.fsPath)) { + args.push(`-op=${optionsPath}`); + } + + return args; +} + +function getExecArgs( + libraryDirectory: Uri, + libraryName: string, + modelPath: Uri | null, + terminologyPath: Uri | null, + contextValue: string | null, + measurementPeriod: string, +): string[] { + // TODO: One day we might support other models and contexts + const modelType = 'FHIR'; + const contextType = 'Patient'; + + let args: string[] = []; + args.push(`-ln=${libraryName}`); + args.push(`-lu=${libraryDirectory}`); + + if (modelPath) { + args.push(`-m=${modelType}`); + args.push(`-mu=${modelPath}`); + } + + if (terminologyPath) { + args.push(`-t=${terminologyPath}`); + } + + if (contextValue) { + args.push(`-c=${contextType}`); + args.push(`-cv=${contextValue}`); + } + + if (measurementPeriod && measurementPeriod !== '') { + args.push(`-p=${libraryName}."Measurement Period"`); + args.push(`-pv=${measurementPeriod}`); + } + + return args; +} diff --git a/src/normalizeCqlExecution.ts b/src/normalizeCqlExecution.ts deleted file mode 100644 index 23b0d93..0000000 --- a/src/normalizeCqlExecution.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Uri, window } from "vscode"; -import { buildParameters } from "./buildParameters"; -import { executeCQL } from "./executeCql"; - -export async function normalizeCqlExecution(uri: Uri) { - - // Needs a distinction between CQL file and single line - const isCqlFile = window.activeTextEditor!.document.fileName.endsWith(".cql"); - - if (isCqlFile) { - // should normalize data - let operationArgs = buildParameters(uri) - executeCQL(operationArgs) - } else { - window.showInformationMessage('As of now we only support Cql File Execution and execution needs to run a .cql file.'); - } - -} - From 5d16e5d1c97b71ea32e3b518d203f36a01703128 Mon Sep 17 00:00:00 2001 From: Madison Swain-Bowden Date: Wed, 28 Aug 2024 20:18:44 -0700 Subject: [PATCH 3/4] Add GitHub Action to run prettier on PRs (#70) * Add GitHub Action to run prettier on PRs * Attempt to run on pull request target too * Attempt to run on branch push too * Only trigger on pull_request * Run on push to master --- .github/workflows/pull_request_checks.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/pull_request_checks.yml diff --git a/.github/workflows/pull_request_checks.yml b/.github/workflows/pull_request_checks.yml new file mode 100644 index 0000000..4819fc3 --- /dev/null +++ b/.github/workflows/pull_request_checks.yml @@ -0,0 +1,18 @@ +name: Prettier linting + +on: + pull_request: + push: + branches: + - master + +jobs: + prettier: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: creyD/prettier_action@v4.3 + with: + # Fail if differences are found + prettier_options: --list-different **/*.{js,ts} From 74a8e17121a3db47b8154c540d3d20f7f623c1fe Mon Sep 17 00:00:00 2001 From: Madison Swain-Bowden Date: Wed, 28 Aug 2024 20:30:30 -0700 Subject: [PATCH 4/4] Run prettier on .eslintrc.js (#72) --- .eslintrc.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0f55471..e0a021a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,19 +1,19 @@ module.exports = { - ignorePatterns: ['**/*.d.ts', '**/*.test.ts', '**/*.js'], - parser: '@typescript-eslint/parser', - extends: ['plugin:@typescript-eslint/recommended'], - plugins: ['header'], - parserOptions: { - ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - }, - rules: { - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-namespace': 'off', - // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs - // e.g. "@typescript-eslint/explicit-function-return-type": "off", - }, - }; \ No newline at end of file + ignorePatterns: ['**/*.d.ts', '**/*.test.ts', '**/*.js'], + parser: '@typescript-eslint/parser', + extends: ['plugin:@typescript-eslint/recommended'], + plugins: ['header'], + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + }, + rules: { + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-namespace': 'off', + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // e.g. "@typescript-eslint/explicit-function-return-type": "off", + }, +};