diff --git a/.vscode/launch.json b/.vscode/launch.json index 3c804f4..848517e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,11 @@ "sourceMaps": true, "preLaunchTask": { "type": "npm", - "script": "compile" + "script": "watch" + }, + "resolveSourceMapLocations": ["${workspaceFolder}/client/dist/**/*.js"], + "sourceMapPathOverrides": { + "webpack://?:*/*": "${workspaceFolder}/client/*" } }, { diff --git a/client/src/browser/extension.ts b/client/src/browser/extension.ts index 6e00e5b..1698493 100644 --- a/client/src/browser/extension.ts +++ b/client/src/browser/extension.ts @@ -9,11 +9,11 @@ let gxFormat2LanguageClient: LanguageClient; export function activate(context: ExtensionContext): void { nativeLanguageClient = createWebWorkerLanguageClient( - Constants.NATIVE_WORKFLOW_LANGUAGE_ID, + [Constants.NATIVE_WORKFLOW_LANGUAGE_ID], Uri.joinPath(context.extensionUri, "server/gx-workflow-ls-native/dist/web/nativeServer.js") ); gxFormat2LanguageClient = createWebWorkerLanguageClient( - Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, + [Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, Constants.GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID], Uri.joinPath(context.extensionUri, "server/gx-workflow-ls-format2/dist/web/gxFormat2Server.js") ); @@ -25,9 +25,14 @@ export async function deactivate(): Promise { await gxFormat2LanguageClient?.stop(); } -function createWebWorkerLanguageClient(languageId: string, serverUri: Uri): LanguageClient { - const documentSelector = [{ language: languageId }]; +function createWebWorkerLanguageClient(languageIds: string[], serverUri: Uri): LanguageClient { + const documentSelector = languageIds.map((languageId) => ({ language: languageId })); const clientOptions: LanguageClientOptions = buildBasicLanguageClientOptions(documentSelector); const worker = new Worker(serverUri.toString()); - return new LanguageClient(`${languageId}-language-client`, `Galaxy Workflows (${languageId})`, clientOptions, worker); + return new LanguageClient( + `${languageIds}-language-client`, + `Galaxy Workflows (${languageIds})`, + clientOptions, + worker + ); } diff --git a/client/src/commands/cleanWorkflow.ts b/client/src/commands/cleanWorkflow.ts index 74a8e23..7ae6040 100644 --- a/client/src/commands/cleanWorkflow.ts +++ b/client/src/commands/cleanWorkflow.ts @@ -1,6 +1,6 @@ import { window } from "vscode"; -import { CleanWorkflowDocumentParams, CleanWorkflowDocumentRequest } from "../common/requestsDefinitions"; import { CustomCommand, getCommandFullIdentifier } from "."; +import { CleanWorkflowDocumentParams, CleanWorkflowDocumentResult, LSRequestIdentifiers } from "../languageTypes"; /** * Command to 'clean' the selected workflow document. @@ -17,7 +17,10 @@ export class CleanWorkflowCommand extends CustomCommand { const { document } = window.activeTextEditor; const params: CleanWorkflowDocumentParams = { uri: this.client.code2ProtocolConverter.asUri(document.uri) }; - const result = await this.client.sendRequest(CleanWorkflowDocumentRequest.type, params); + const result = await this.client.sendRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT, + params + ); if (!result) { throw new Error("Cannot clean the requested document. The server returned no result."); } diff --git a/client/src/common/constants.ts b/client/src/common/constants.ts index c2b0d8a..2834e5d 100644 --- a/client/src/common/constants.ts +++ b/client/src/common/constants.ts @@ -2,5 +2,6 @@ export namespace Constants { export const NATIVE_WORKFLOW_LANGUAGE_ID = "galaxyworkflow"; export const GXFORMAT2_WORKFLOW_LANGUAGE_ID = "gxformat2"; + export const GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID = "gxwftests"; export const CLEAN_WORKFLOW_DOCUMENT_SCHEME = "galaxy-clean-workflow"; } diff --git a/client/src/common/index.ts b/client/src/common/index.ts index d57df53..b88c346 100644 --- a/client/src/common/index.ts +++ b/client/src/common/index.ts @@ -5,6 +5,7 @@ import { CleanWorkflowDocumentProvider } from "../providers/cleanWorkflowDocumen import { CleanWorkflowProvider } from "../providers/cleanWorkflowProvider"; import { GitProvider } from "../providers/git"; import { BuiltinGitProvider } from "../providers/git/gitProvider"; +import { setupRequests } from "../requests/gxworkflows"; export function buildBasicLanguageClientOptions(documentSelector: DocumentSelector): LanguageClientOptions { // Options to control the language client @@ -30,6 +31,8 @@ export function initExtension( // Setup gxformat2 language features startLanguageClient(context, gxFormat2Client); + + setupRequests(context, nativeClient, gxFormat2Client); } function initGitProvider(context: ExtensionContext): BuiltinGitProvider { diff --git a/client/src/common/requestsDefinitions.ts b/client/src/common/requestsDefinitions.ts deleted file mode 100644 index d3f9eb1..0000000 --- a/client/src/common/requestsDefinitions.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -import { RequestType } from "vscode-languageclient"; - -// TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 - -export namespace LSRequestIdentifiers { - export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; - export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; -} - -export interface CleanWorkflowDocumentParams { - uri: string; -} - -export interface CleanWorkflowDocumentResult { - error: string; -} - -export interface CleanWorkflowContentsParams { - contents: string; -} - -export interface CleanWorkflowContentsResult { - contents: string; -} - -export namespace CleanWorkflowDocumentRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT - ); -} - -export namespace CleanWorkflowContentsRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS - ); -} diff --git a/client/src/common/utils.ts b/client/src/common/utils.ts index aa1d3fb..1d69a51 100644 --- a/client/src/common/utils.ts +++ b/client/src/common/utils.ts @@ -5,7 +5,7 @@ import { OutputChannel, Uri, workspace } from "vscode"; * @returns true if the workspace is not mounted on a regular filesystem. */ export function isVirtualWorkspace(): boolean { - return workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== "file"); + return (workspace.workspaceFolders ?? []).every((f) => f.uri.scheme !== "file"); } /** @@ -45,3 +45,31 @@ export function debugPrintCommandArgs(command: string, args: unknown[], outputCh } outputChannel.appendLine(`---\n`); } + +export function isWorkflowTestsDocument(uri: Uri): boolean { + return uri.path.endsWith("-test.yml") || uri.path.endsWith("-tests.yml"); +} + +export function isNativeWorkflowDocument(uri: Uri): boolean { + return uri.path.endsWith(".ga"); +} + +export async function getAssociatedWorkflowUriFromTestsUri(workflowTestsDocumentUri: Uri): Promise { + const format2WorkflowUri = Uri.parse( + workflowTestsDocumentUri.toString().replace("-test.yml", ".gxwf.yml").replace("-tests.yml", ".gxwf.yml") + ); + try { + await workspace.fs.stat(format2WorkflowUri); + return format2WorkflowUri; + } catch { + const nativeWorkflowUri = Uri.parse( + workflowTestsDocumentUri.toString().replace("-test.yml", ".ga").replace("-tests.yml", ".ga") + ); + try { + await workspace.fs.stat(nativeWorkflowUri); + return nativeWorkflowUri; + } catch { + return undefined; + } + } +} diff --git a/client/src/extension.ts b/client/src/extension.ts index 3c510eb..8eae430 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -10,11 +10,11 @@ let gxFormat2LanguageClient: LanguageClient; export function activate(context: ExtensionContext): void { nativeLanguageClient = buildNodeLanguageClient( - Constants.NATIVE_WORKFLOW_LANGUAGE_ID, + [Constants.NATIVE_WORKFLOW_LANGUAGE_ID], buildNativeServerOptions(context) ); gxFormat2LanguageClient = buildNodeLanguageClient( - Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, + [Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, Constants.GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID], buildGxFormat2ServerOptions(context) ); @@ -26,12 +26,12 @@ export async function deactivate(): Promise { await gxFormat2LanguageClient?.stop(); } -function buildNodeLanguageClient(languageId: string, serverOptions: ServerOptions): LanguageClient { - const documentSelector = [{ language: languageId }]; +function buildNodeLanguageClient(languageIds: string[], serverOptions: ServerOptions): LanguageClient { + const documentSelector = languageIds.map((languageId) => ({ language: languageId })); const clientOptions: LanguageClientOptions = buildBasicLanguageClientOptions(documentSelector); return new LanguageClient( - `${languageId}-language-client`, - `Galaxy Workflows (${languageId})`, + `${languageIds}-language-client`, + `Galaxy Workflows (${languageIds})`, serverOptions, clientOptions ); diff --git a/client/src/languageTypes.ts b/client/src/languageTypes.ts new file mode 100644 index 0000000..d50923f --- /dev/null +++ b/client/src/languageTypes.ts @@ -0,0 +1,21 @@ +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +} from "../../shared/src/requestsDefinitions"; + +export { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +}; diff --git a/client/src/providers/cleanWorkflowProvider.ts b/client/src/providers/cleanWorkflowProvider.ts index 69ea414..4efcae7 100644 --- a/client/src/providers/cleanWorkflowProvider.ts +++ b/client/src/providers/cleanWorkflowProvider.ts @@ -1,7 +1,7 @@ import { Uri, window, workspace } from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; -import { CleanWorkflowContentsParams, CleanWorkflowContentsRequest } from "../common/requestsDefinitions"; import { getWorkspaceScheme, replaceUriScheme } from "../common/utils"; +import { CleanWorkflowContentsParams, CleanWorkflowContentsResult, LSRequestIdentifiers } from "../languageTypes"; import { GitProvider } from "./git"; /** @@ -56,7 +56,10 @@ export class CleanWorkflowProvider { const params: CleanWorkflowContentsParams = { contents: contents, }; - const result = await this.languageClient.sendRequest(CleanWorkflowContentsRequest.type, params); + const result = await this.languageClient.sendRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS, + params + ); if (!result) { throw new Error("Cannot clean the requested document contents. The server returned no content"); } diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts new file mode 100644 index 0000000..baad2f4 --- /dev/null +++ b/client/src/requests/gxworkflows.ts @@ -0,0 +1,57 @@ +import { ExtensionContext, Uri, workspace } from "vscode"; +import { BaseLanguageClient } from "vscode-languageclient"; +import { + getAssociatedWorkflowUriFromTestsUri, + isNativeWorkflowDocument, + isWorkflowTestsDocument, +} from "../common/utils"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +} from "../languageTypes"; + +export function setupRequests( + context: ExtensionContext, + nativeWorkflowClient: BaseLanguageClient, + gxFormat2Client: BaseLanguageClient +): void { + function createRequestHandler(requestIdentifier: string) { + return async (params: TargetWorkflowDocumentParams) => { + let targetUri: Uri | undefined = Uri.parse(params.uri); + if (isWorkflowTestsDocument(targetUri)) { + // If the target is a test file, we need to find the associated workflow file + targetUri = await getAssociatedWorkflowUriFromTestsUri(targetUri); + } + if (!targetUri) { + console.debug("No associated workflow file found for:", params.uri); + return undefined; + } + // Open the file to include it in the document cache + await workspace.openTextDocument(targetUri); + + let languageClient = gxFormat2Client; + if (isNativeWorkflowDocument(targetUri)) { + languageClient = nativeWorkflowClient; + } + const requestParams: TargetWorkflowDocumentParams = { uri: targetUri.toString() }; + const result = await languageClient.sendRequest(requestIdentifier, requestParams); + return result; + }; + } + + context.subscriptions.push( + gxFormat2Client.onRequest( + LSRequestIdentifiers.GET_WORKFLOW_INPUTS, + createRequestHandler(LSRequestIdentifiers.GET_WORKFLOW_INPUTS) + ) + ); + + context.subscriptions.push( + gxFormat2Client.onRequest( + LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, + createRequestHandler(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS) + ) + ); +} diff --git a/package-lock.json b/package-lock.json index b523f4d..fe78658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,14 @@ "hasInstallScript": true, "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.4", + "@types/jest": "^29.5.8", "@types/mocha": "^10.0.1", "@types/vscode": "^1.81.0", "@types/webpack-env": "^1.18.1", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", - "@vscode/test-electron": "^2.3.10", + "@vscode/test-electron": "^2.3.4", + "@vscode/test-web": "^0.0.54", "assert": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", @@ -64,18 +65,89 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.17.10", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", @@ -125,14 +197,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -140,14 +212,14 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" @@ -181,34 +253,34 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -267,12 +339,12 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -288,9 +360,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -320,15 +392,14 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" @@ -406,9 +477,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -607,34 +678,34 @@ } }, "node_modules/@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", - "debug": "^4.3.1", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", "globals": "^11.1.0" }, "engines": { @@ -651,13 +722,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1366,6 +1437,34 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "dev": true, + "dependencies": { + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@koa/router": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", + "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.2.1" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1431,6 +1530,19 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/browser-chromium": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.44.1.tgz", + "integrity": "sha512-ixlllsDKMzeCfU0GvlneqUtPu2jqak5r8BD0kRyJO5gNkOMa9UxippK1Ubgi7yqWn4RXvxFRoLSOeh/w6PHFrQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1565,9 +1677,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -1835,6 +1947,72 @@ "node": ">=16" } }, + "node_modules/@vscode/test-web": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.54.tgz", + "integrity": "sha512-LDdFFEzmnKQ96QmuRxK9kRhddQmwqyI/VOb6YhyPqOsbKBIM7uzTqpckoueOu9EEnvPSNQNoxqb426AakI8e5w==", + "dev": true, + "dependencies": { + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "@playwright/browser-chromium": "^1.43.1", + "gunzip-maybe": "^1.4.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "koa": "^2.15.3", + "koa-morgan": "^1.0.1", + "koa-mount": "^4.0.0", + "koa-static": "^5.0.0", + "minimist": "^1.2.8", + "playwright": "^1.43.1", + "tar-fs": "^3.0.5", + "vscode-uri": "^3.0.8" + }, + "bin": { + "vscode-test-web": "out/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/test-web/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vscode/test-web/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vscode/test-web/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -2037,6 +2215,19 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2216,6 +2407,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", @@ -2338,6 +2535,70 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^1.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.16.1" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/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==", + "dev": true + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -2405,6 +2666,21 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, + "dependencies": { + "pako": "~0.2.0" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true + }, "node_modules/browserslist": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", @@ -2476,6 +2752,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2780,6 +3069,27 @@ "node": ">=12" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -2795,6 +3105,19 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2874,6 +3197,12 @@ } } }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3057,6 +3386,31 @@ "node": ">= 0.4" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3108,12 +3462,30 @@ "node": ">=6.0.0" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.135", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.135.tgz", @@ -3147,6 +3519,24 @@ "node": ">= 4" } }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -3253,6 +3643,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3569,6 +3965,12 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3750,6 +4152,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3930,6 +4341,23 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, + "dependencies": { + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" + }, + "bin": { + "gunzip-maybe": "bin.js" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4002,6 +4430,69 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -4237,6 +4728,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4306,6 +4803,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -5392,6 +5898,18 @@ "setimmediate": "^1.0.5" } }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5410,6 +5928,185 @@ "node": ">=6" } }, + "node_modules/koa": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", + "dev": true, + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", + "dev": true, + "dependencies": { + "morgan": "^1.6.1" + } + }, + "node_modules/koa-mount": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "dev": true, + "dependencies": { + "debug": "^4.0.1", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/koa-send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-send/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-send/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5561,6 +6258,15 @@ "tmpl": "1.0.5" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -5588,6 +6294,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -5643,6 +6358,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", @@ -5734,6 +6458,49 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5758,6 +6525,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -5849,6 +6625,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5873,6 +6670,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, "node_modules/open": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", @@ -5983,6 +6786,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -6047,6 +6859,12 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6056,6 +6874,17 @@ "node": ">=8" } }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6147,6 +6976,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6237,6 +7096,27 @@ "node": ">= 6" } }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6282,6 +7162,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6404,6 +7290,64 @@ "node": ">=4" } }, + "node_modules/resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "dependencies": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-path/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/resolve-path/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/resolve-path/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -6602,6 +7546,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -6727,8 +7677,36 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "engines": { - "node": ">=8" + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -6918,6 +7896,41 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-fs/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/terser": { "version": "5.19.3", "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.3.tgz", @@ -6999,6 +8012,16 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -7038,6 +8061,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -7136,6 +8168,15 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true, + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7169,6 +8210,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -7249,6 +8303,21 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7511,6 +8580,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7591,6 +8669,15 @@ "node": ">=10" } }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7622,13 +8709,71 @@ } }, "@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -7669,26 +8814,26 @@ } }, "@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "requires": { - "@babel/types": "^7.24.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" } } } @@ -7714,28 +8859,28 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -7779,12 +8924,12 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { @@ -7794,9 +8939,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -7817,15 +8962,14 @@ } }, "@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "dependencies": { "ansi-styles": { @@ -7887,9 +9031,9 @@ } }, "@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -8028,31 +9172,31 @@ } }, "@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", - "debug": "^4.3.1", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { @@ -8065,13 +9209,13 @@ } }, "@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -8614,6 +9758,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "dev": true, + "requires": { + "vary": "^1.1.2" + } + }, + "@koa/router": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", + "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.2.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8661,6 +9827,15 @@ "tslib": "^2.6.0" } }, + "@playwright/browser-chromium": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.44.1.tgz", + "integrity": "sha512-ixlllsDKMzeCfU0GvlneqUtPu2jqak5r8BD0kRyJO5gNkOMa9UxippK1Ubgi7yqWn4RXvxFRoLSOeh/w6PHFrQ==", + "dev": true, + "requires": { + "playwright-core": "1.44.1" + } + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -8792,9 +9967,9 @@ } }, "@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "requires": { "expect": "^29.0.0", @@ -8970,6 +10145,59 @@ "semver": "^7.5.2" } }, + "@vscode/test-web": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.54.tgz", + "integrity": "sha512-LDdFFEzmnKQ96QmuRxK9kRhddQmwqyI/VOb6YhyPqOsbKBIM7uzTqpckoueOu9EEnvPSNQNoxqb426AakI8e5w==", + "dev": true, + "requires": { + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "@playwright/browser-chromium": "^1.43.1", + "gunzip-maybe": "^1.4.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "koa": "^2.15.3", + "koa-morgan": "^1.0.1", + "koa-mount": "^4.0.0", + "koa-static": "^5.0.0", + "minimist": "^1.2.8", + "playwright": "^1.43.1", + "tar-fs": "^3.0.5", + "vscode-uri": "^3.0.8" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -9149,6 +10377,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -9275,6 +10513,12 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", @@ -9372,6 +10616,69 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^1.0.0" + } + }, + "bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^2.1.0" + } + }, + "bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "dev": true, + "optional": true, + "requires": { + "streamx": "^2.16.1" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -9424,6 +10731,23 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, + "requires": { + "pako": "~0.2.0" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true + } + } + }, "browserslist": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", @@ -9470,6 +10794,16 @@ "run-applescript": "^5.0.0" } }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9686,6 +11020,21 @@ } } }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -9703,6 +11052,16 @@ } } }, + "cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -9751,6 +11110,12 @@ "dev": true, "requires": {} }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9867,6 +11232,24 @@ "object-keys": "^1.0.12" } }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -9900,7 +11283,19 @@ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { - "esutils": "^2.0.2" + "esutils": "^2.0.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, "eastasianwidth": { @@ -9909,6 +11304,12 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "electron-to-chromium": { "version": "1.4.135", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.135.tgz", @@ -9933,6 +11334,21 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -10015,6 +11431,12 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -10234,6 +11656,12 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -10379,6 +11807,12 @@ } } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10507,6 +11941,20 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, + "requires": { + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -10555,6 +12003,56 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -10721,6 +12219,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true + }, "is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -10763,6 +12267,12 @@ "is-extglob": "^2.1.1" } }, + "is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true + }, "is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -11568,6 +13078,15 @@ "setimmediate": "^1.0.5" } }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -11580,6 +13099,160 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "koa": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "requires": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + } + }, + "koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", + "dev": true, + "requires": { + "morgan": "^1.6.1" + } + }, + "koa-mount": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "dev": true, + "requires": { + "debug": "^4.0.1", + "koa-compose": "^4.1.0" + } + }, + "koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11698,6 +13371,12 @@ "tmpl": "1.0.5" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, "merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -11719,6 +13398,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -11759,6 +13444,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, "minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", @@ -11829,6 +13520,45 @@ } } }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11847,6 +13577,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -11914,6 +13650,21 @@ "object-keys": "^1.1.1" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11932,6 +13683,12 @@ "mimic-fn": "^2.1.0" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, "open": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", @@ -12009,6 +13766,12 @@ "lines-and-columns": "^1.1.6" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -12057,12 +13820,29 @@ } } }, + "path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -12129,6 +13909,22 @@ } } }, + "playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.44.1" + } + }, + "playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12191,6 +13987,27 @@ "sisteransi": "^1.0.5" } }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -12209,6 +14026,12 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12311,6 +14134,54 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "requires": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, "resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -12433,6 +14304,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -12537,6 +14414,29 @@ } } }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12673,6 +14573,41 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "terser": { "version": "5.19.3", "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.3.tgz", @@ -12726,6 +14661,16 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -12753,6 +14698,12 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12808,6 +14759,12 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12829,6 +14786,16 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -12893,6 +14860,18 @@ "convert-source-map": "^1.6.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -13075,6 +15054,12 @@ "signal-exit": "^3.0.7" } }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13137,6 +15122,12 @@ "is-plain-obj": "^2.1.0" } }, + "ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 9d286db..5e674c2 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,16 @@ ".gxwf.yml" ], "configuration": "./workflow-languages/configurations/yml.language-configuration.json" + }, + { + "id": "gxwftests", + "aliases": [ + "Galaxy Workflow Tests (YAML)" + ], + "extensions": [ + "-tests.yml" + ], + "configuration": "./workflow-languages/configurations/yml.language-configuration.json" } ], "grammars": [ @@ -72,6 +82,11 @@ "language": "gxformat2", "scopeName": "source.gxformat2", "path": "./workflow-languages/syntaxes/yml.tmLanguage.json" + }, + { + "language": "gxwftests", + "scopeName": "source.gxformat2", + "path": "./workflow-languages/syntaxes/yml.tmLanguage.json" } ], "configuration": [ @@ -207,21 +222,23 @@ "watch": "concurrently --kill-others \"npm run watch-server\" \"npm run watch-client\"", "watch-server": "cd server && npm run watch", "watch-client": "cd client && npm run watch", - "test": "jest", + "test": "npm run test-client && npm run test-server", "test-client": "cd client && npm test", "test-server": "cd server && npm test", "test-compile": "tsc --project ./client --outDir client/out", "pretest:e2e": "npm run compile && npm run test-compile", - "test:e2e": "node ./client/out/e2e/runTests.js" + "test:e2e": "node ./client/out/e2e/runTests.js", + "test-browser": "vscode-test-web --extensionDevelopmentPath=. ./test-data" }, "devDependencies": { - "@types/jest": "^29.5.4", + "@types/jest": "^29.5.8", "@types/mocha": "^10.0.1", "@types/vscode": "^1.81.0", "@types/webpack-env": "^1.18.1", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", - "@vscode/test-electron": "^2.3.10", + "@vscode/test-electron": "^2.3.4", + "@vscode/test-web": "^0.0.54", "assert": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", diff --git a/server/gx-workflow-ls-format2/package.json b/server/gx-workflow-ls-format2/package.json index 94e422d..0f21b4c 100644 --- a/server/gx-workflow-ls-format2/package.json +++ b/server/gx-workflow-ls-format2/package.json @@ -6,11 +6,17 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" }, - "scripts": {} + "scripts": {}, + "devDependencies": { + "@types/jest": "^29.5.8" + } } diff --git a/server/gx-workflow-ls-format2/src/browser/server.ts b/server/gx-workflow-ls-format2/src/browser/server.ts index 8e4d705..76b38fc 100644 --- a/server/gx-workflow-ls-format2/src/browser/server.ts +++ b/server/gx-workflow-ls-format2/src/browser/server.ts @@ -1,12 +1,21 @@ -import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { GxFormat2WorkflowLanguageService } from "../languageService"; +import { + createConnection, + BrowserMessageReader, + BrowserMessageWriter, + Connection, +} from "vscode-languageserver/browser"; +import { TYPES, container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/languageTypes"; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +function createBrowserConnection(): Connection { + const messageReader = new BrowserMessageReader(self); + const messageWriter = new BrowserMessageWriter(self); -const connection = createConnection(messageReader, messageWriter); + const connection = createConnection(messageReader, messageWriter); + return connection; +} -const languageService = new GxFormat2WorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +container.bind(TYPES.Connection).toConstantValue(createBrowserConnection()); + +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 633c817..72168e5 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,4 +1,11 @@ -import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; +import { PropertyASTNode } from "@gxwf/server-common/src/ast/types"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + TextDocument, + WorkflowDataType, + WorkflowDocument, +} from "@gxwf/server-common/src/languageTypes"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; /** @@ -14,4 +21,66 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { public get yamlDocument(): YAMLDocument { return this._yamlDocument; } + + public getWorkflowInputs(): GetWorkflowInputsResult { + const result: GetWorkflowInputsResult = { inputs: [] }; + const inputs = this.nodeManager.getNodeFromPath("inputs"); + if (inputs?.type === "property") { + const inputList = inputs.valueNode?.children; + if (inputList) { + inputList.forEach((input) => { + if (input.type !== "property" || !input.keyNode) return; + const inputName = String(input.keyNode.value); + const inputType = this.extractInputType(input); + const inputDocNode = input.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "doc" + ) as PropertyASTNode; + const inputDescription = String(inputDocNode?.valueNode?.value ?? ""); + result.inputs.push({ + name: inputName, + doc: inputDescription, + type: inputType, + }); + }); + } + } + return result; + } + + public getWorkflowOutputs(): GetWorkflowOutputsResult { + const result: GetWorkflowOutputsResult = { outputs: [] }; + const output = this.nodeManager.getNodeFromPath("outputs"); + if (output?.type === "property") { + const outputList = output.valueNode?.children; + if (outputList) { + outputList.forEach((output) => { + if (output.type !== "property" || !output.keyNode) return; + const outputName = String(output.keyNode.value); + const outputDocNode = output.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "doc" + ) as PropertyASTNode; + const outputDoc = String(outputDocNode?.valueNode?.value ?? ""); + result.outputs.push({ + name: outputName, + doc: outputDoc, + }); + }); + } + } + return result; + } + + private extractInputType(input: PropertyASTNode): WorkflowDataType { + let inputType: WorkflowDataType = "data"; + const inputTypeNode = input.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "type" + ) as PropertyASTNode; + if (inputTypeNode) { + inputType = String(inputTypeNode.valueNode?.value) as WorkflowDataType; + } else { + // If the type property is not specified, it might be defined in the value node itself + inputType = input.valueNode?.value as WorkflowDataType; + } + return inputType; + } } diff --git a/server/gx-workflow-ls-format2/src/inversify.config.ts b/server/gx-workflow-ls-format2/src/inversify.config.ts new file mode 100644 index 0000000..e84c1a2 --- /dev/null +++ b/server/gx-workflow-ls-format2/src/inversify.config.ts @@ -0,0 +1,26 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { GxFormat2WorkflowLanguageServiceImpl } from "./languageService"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { GalaxyWorkflowLanguageServer, WorkflowLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { GalaxyWorkflowLanguageServerImpl } from "@gxwf/server-common/src/server"; +import { TYPES as COMMON_TYPES } from "@gxwf/server-common/src/languageTypes"; +import { YAMLLanguageServiceContainerModule } from "@gxwf/yaml-language-service/src/inversify.config"; + +export const TYPES = { + ...COMMON_TYPES, +}; + +container.load(YAMLLanguageServiceContainerModule); +container.load(WorkflowTestsLanguageServiceContainerModule); + +container + .bind(TYPES.WorkflowLanguageService) + .to(GxFormat2WorkflowLanguageServiceImpl) + .inSingletonScope(); + +container + .bind(TYPES.GalaxyWorkflowLanguageServer) + .to(GalaxyWorkflowLanguageServerImpl) + .inSingletonScope(); + +export { container }; diff --git a/server/gx-workflow-ls-format2/src/languageService.ts b/server/gx-workflow-ls-format2/src/languageService.ts index bfffb2a..d75d993 100644 --- a/server/gx-workflow-ls-format2/src/languageService.ts +++ b/server/gx-workflow-ls-format2/src/languageService.ts @@ -3,35 +3,46 @@ import { Range, FormattingOptions, TextEdit, - WorkflowDocument, - WorkflowLanguageService, + LanguageServiceBase, Position, Hover, CompletionList, Diagnostic, WorkflowValidator, + LanguageService, } from "@gxwf/server-common/src/languageTypes"; -import { LanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; import { GxFormat2WorkflowDocument } from "./gxFormat2WorkflowDocument"; import { GalaxyWorkflowFormat2SchemaLoader } from "./schema"; import { GxFormat2CompletionService } from "./services/completionService"; import { GxFormat2HoverService } from "./services/hoverService"; import { GxFormat2SchemaValidationService, WorkflowValidationService } from "./services/validation"; +import { inject, injectable } from "inversify"; +import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; + +const LANGUAGE_ID = "gxformat2"; + +export interface GxFormat2WorkflowLanguageService extends LanguageService {} /** * A wrapper around the YAML Language Service to support language features * for gxformat2 Galaxy workflow files. */ -export class GxFormat2WorkflowLanguageService extends WorkflowLanguageService { - private _yamlLanguageService: LanguageService; +@injectable() +export class GxFormat2WorkflowLanguageServiceImpl + extends LanguageServiceBase + implements GxFormat2WorkflowLanguageService +{ + private _yamlLanguageService: YAMLLanguageService; private _schemaLoader: GalaxyWorkflowFormat2SchemaLoader; private _hoverService: GxFormat2HoverService; private _completionService: GxFormat2CompletionService; private _validationServices: WorkflowValidator[]; - constructor() { - super(); + + constructor(@inject(YAML_TYPES.YAMLLanguageService) yamlLanguageService: YAMLLanguageService) { + super(LANGUAGE_ID); this._schemaLoader = new GalaxyWorkflowFormat2SchemaLoader(); - this._yamlLanguageService = getLanguageService(); + this._yamlLanguageService = yamlLanguageService; this._hoverService = new GxFormat2HoverService(this._schemaLoader.nodeResolver); this._completionService = new GxFormat2CompletionService(this._schemaLoader.nodeResolver); this._validationServices = [ @@ -40,7 +51,7 @@ export class GxFormat2WorkflowLanguageService extends WorkflowLanguageService { ]; } - public override parseWorkflowDocument(document: TextDocument): WorkflowDocument { + public override parseDocument(document: TextDocument): GxFormat2WorkflowDocument { const yamlDocument = this._yamlLanguageService.parseYAMLDocument(document); return new GxFormat2WorkflowDocument(document, yamlDocument); } @@ -49,22 +60,22 @@ export class GxFormat2WorkflowLanguageService extends WorkflowLanguageService { return this._yamlLanguageService.doFormat(document, options); } - public override doHover(workflowDocument: WorkflowDocument, position: Position): Promise { - return this._hoverService.doHover(workflowDocument.textDocument, position, workflowDocument.nodeManager); + public override doHover(documentContext: GxFormat2WorkflowDocument, position: Position): Promise { + return this._hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager); } public override async doComplete( - workflowDocument: WorkflowDocument, + documentContext: GxFormat2WorkflowDocument, position: Position ): Promise { - return this._completionService.doComplete(workflowDocument.textDocument, position, workflowDocument.nodeManager); + return this._completionService.doComplete(documentContext.textDocument, position, documentContext.nodeManager); } - protected override async doValidation(workflowDocument: WorkflowDocument): Promise { - const format2WorkflowDocument = workflowDocument as GxFormat2WorkflowDocument; + protected override async doValidation(documentContext: GxFormat2WorkflowDocument): Promise { + const format2WorkflowDocument = documentContext as GxFormat2WorkflowDocument; const diagnostics = await this._yamlLanguageService.doValidation(format2WorkflowDocument.yamlDocument); for (const validator of this._validationServices) { - const results = await validator.doValidation(workflowDocument); + const results = await validator.doValidation(documentContext); diagnostics.push(...results); } return diagnostics; diff --git a/server/gx-workflow-ls-format2/src/node/server.ts b/server/gx-workflow-ls-format2/src/node/server.ts index 2ec4627..34edc9b 100644 --- a/server/gx-workflow-ls-format2/src/node/server.ts +++ b/server/gx-workflow-ls-format2/src/node/server.ts @@ -1,9 +1,8 @@ -import { createConnection } from "vscode-languageserver/node"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { GxFormat2WorkflowLanguageService } from "../languageService"; +import { Connection, createConnection } from "vscode-languageserver/node"; +import { container, TYPES } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/languageTypes"; -const connection = createConnection(); +container.bind(TYPES.Connection).toConstantValue(createConnection()); -const languageService = new GxFormat2WorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts b/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts index 0b3acaa..6ffd7be 100644 --- a/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts +++ b/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts @@ -12,10 +12,15 @@ import { SchemaField, SchemaRecord, } from "./definitions"; -import { SchemaNodeResolver } from "./schemaNodeResolver"; +import { SchemaNodeResolver, SchemaNodeResolverImpl } from "./schemaNodeResolver"; import { SCHEMA_DOCS_v19_09_MAP } from "./versions"; -export class GalaxyWorkflowFormat2SchemaLoader { +export interface GalaxyWorkflowSchemaLoader { + readonly definitions: SchemaDefinitions; + readonly nodeResolver: SchemaNodeResolver; +} + +export class GalaxyWorkflowFormat2SchemaLoader implements GalaxyWorkflowSchemaLoader { private _documentEntryMap = new Map>(); private _rawSchemaEntries = new Map(); private _namespaces = new Map(); @@ -250,7 +255,7 @@ export class GalaxyWorkflowFormat2SchemaLoader { } private createNodeResolver(): SchemaNodeResolver { - return new SchemaNodeResolver(this.definitions, this._root); + return new SchemaNodeResolverImpl(this.definitions, this._root); } /** Expands all entries with the types defined in the extended types.*/ diff --git a/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts b/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts index aa8ba31..bf2f12d 100644 --- a/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts +++ b/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts @@ -1,7 +1,14 @@ import { NodePath, Segment } from "@gxwf/server-common/src/ast/types"; import { RecordSchemaNode, SchemaDefinitions, SchemaNode, SchemaRecord } from "./definitions"; -export class SchemaNodeResolver { +export interface SchemaNodeResolver { + rootNode: SchemaNode; + definitions: SchemaDefinitions; + resolveSchemaContext(path: NodePath): SchemaNode | undefined; + getSchemaNodeByTypeRef(typeRef: string): SchemaNode | undefined; +} + +export class SchemaNodeResolverImpl implements SchemaNodeResolver { public readonly rootNode: SchemaNode; constructor( public readonly definitions: SchemaDefinitions, diff --git a/server/gx-workflow-ls-format2/src/schema/versions.ts b/server/gx-workflow-ls-format2/src/schema/versions.ts index ed737d6..06736f2 100644 --- a/server/gx-workflow-ls-format2/src/schema/versions.ts +++ b/server/gx-workflow-ls-format2/src/schema/versions.ts @@ -1,7 +1,3 @@ -//TODO: remove this reference when https://github.com/sumwatshade/jest-transform-yaml/pull/20 is merged and upgrade to jest 28 -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import schema_v19_09_workflows from "@schemas/gxformat2/v19_09/workflows.yaml"; import schema_common_metaschema_base from "@schemas/gxformat2/common/metaschema/metaschema_base.yaml"; import schema_v19_09_process from "@schemas/gxformat2/v19_09/process.yaml"; diff --git a/server/gx-workflow-ls-format2/tests/integration/document.test.ts b/server/gx-workflow-ls-format2/tests/integration/document.test.ts new file mode 100644 index 0000000..d449b3c --- /dev/null +++ b/server/gx-workflow-ls-format2/tests/integration/document.test.ts @@ -0,0 +1,82 @@ +import { createFormat2WorkflowDocument } from "../testHelpers"; + +describe("GxFormat2WorkflowDocument", () => { + it("should get workflow inputs", () => { + const TEST_WORKFLOW_CONTENT = ` +class: GalaxyWorkflow +inputs: + input_1: data + input_2: + type: File + doc: This is the input 2 + the_collection: + type: collection + doc: This is a collection + input_int: integer + text_param: + optional: true + default: text value + restrictOnConnections: true + type: text + `; + const document = createFormat2WorkflowDocument(TEST_WORKFLOW_CONTENT); + + const result = document.getWorkflowInputs(); + + expect(result.inputs.length).toBe(5); + expect(result.inputs).toEqual([ + { + name: "input_1", + doc: "", + type: "data", + }, + { + name: "input_2", + doc: "This is the input 2", + type: "File", + }, + { + name: "the_collection", + doc: "This is a collection", + type: "collection", + }, + { + name: "input_int", + doc: "", + type: "integer", + }, + { + name: "text_param", + doc: "", + type: "text", + }, + ]); + }); + + it("should get workflow outputs", () => { + const TEST_WORKFLOW_CONTENT = ` +class: GalaxyWorkflow +outputs: + output_1: + outputSource: second_cat/out_file1 + output_2: + outputSource: first_cat/out_file2 + doc: This is the output 2 + `; + const document = createFormat2WorkflowDocument(TEST_WORKFLOW_CONTENT); + + const result = document.getWorkflowOutputs(); + + expect(result.outputs.length).toBe(2); + expect(result.outputs).toEqual([ + { + name: "output_1", + doc: "", + }, + { + name: "output_2", + doc: "This is the output 2", + }, + ]); + }); +}); diff --git a/server/gx-workflow-ls-format2/tsconfig.json b/server/gx-workflow-ls-format2/tsconfig.json index 3d42a4a..44fa97c 100644 --- a/server/gx-workflow-ls-format2/tsconfig.json +++ b/server/gx-workflow-ls-format2/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/server/gx-workflow-ls-native/package.json b/server/gx-workflow-ls-native/package.json index 1b62fa4..5f28d56 100644 --- a/server/gx-workflow-ls-native/package.json +++ b/server/gx-workflow-ls-native/package.json @@ -6,16 +6,22 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", + "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-languageserver": "^8.1.0", "vscode-uri": "^3.0.7" }, "scripts": { "webpack": "webpack", "watch": "webpack --watch --progress", "test-unit": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.8" } } diff --git a/server/gx-workflow-ls-native/src/browser/server.ts b/server/gx-workflow-ls-native/src/browser/server.ts index 4cf84d4..e134244 100644 --- a/server/gx-workflow-ls-native/src/browser/server.ts +++ b/server/gx-workflow-ls-native/src/browser/server.ts @@ -1,12 +1,21 @@ -import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; -import { NativeWorkflowLanguageService } from "../languageService"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; +import { + createConnection, + BrowserMessageReader, + BrowserMessageWriter, + Connection, +} from "vscode-languageserver/browser"; +import { container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer, TYPES } from "@gxwf/server-common/src/languageTypes"; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +function createBrowserConnection(): Connection { + const messageReader = new BrowserMessageReader(self); + const messageWriter = new BrowserMessageWriter(self); -const connection = createConnection(messageReader, messageWriter); + const connection = createConnection(messageReader, messageWriter); + return connection; +} -const languageService = new NativeWorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +container.bind(TYPES.Connection).toConstantValue(createBrowserConnection()); + +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-native/src/inversify.config.ts b/server/gx-workflow-ls-native/src/inversify.config.ts new file mode 100644 index 0000000..2312ea0 --- /dev/null +++ b/server/gx-workflow-ls-native/src/inversify.config.ts @@ -0,0 +1,22 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { NativeWorkflowLanguageServiceImpl } from "./languageService"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { GalaxyWorkflowLanguageServer, WorkflowLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { GalaxyWorkflowLanguageServerImpl } from "@gxwf/server-common/src/server"; +import { TYPES } from "@gxwf/server-common/src/languageTypes"; +import { YAMLLanguageServiceContainerModule } from "@gxwf/yaml-language-service/src/inversify.config"; + +container.load(YAMLLanguageServiceContainerModule); +container.load(WorkflowTestsLanguageServiceContainerModule); + +container + .bind(TYPES.WorkflowLanguageService) + .to(NativeWorkflowLanguageServiceImpl) + .inSingletonScope(); + +container + .bind(TYPES.GalaxyWorkflowLanguageServer) + .to(GalaxyWorkflowLanguageServerImpl) + .inSingletonScope(); + +export { container }; diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index 33c616d..c8c9284 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -2,7 +2,7 @@ import { DocumentLanguageSettings, getLanguageService, JSONSchema, - LanguageService, + LanguageService as JSONLanguageService, LanguageServiceParams, LanguageSettings, SchemaConfiguration, @@ -16,22 +16,31 @@ import { Range, TextDocument, TextEdit, - WorkflowDocument, - WorkflowLanguageService, + LanguageServiceBase, + LanguageService, } from "@gxwf/server-common/src/languageTypes"; import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.schema.json"; import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; +import { injectable } from "inversify"; + +const LANGUAGE_ID = "galaxyworkflow"; + +export interface NativeWorkflowLanguageService extends LanguageService {} /** * A wrapper around the JSON Language Service to support language features * for native Galaxy workflow files AKA '.ga' workflows. */ -export class NativeWorkflowLanguageService extends WorkflowLanguageService { - private _jsonLanguageService: LanguageService; +@injectable() +export class NativeWorkflowLanguageServiceImpl + extends LanguageServiceBase + implements NativeWorkflowLanguageService +{ + private _jsonLanguageService: JSONLanguageService; private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" }; constructor() { - super(); + super(LANGUAGE_ID); const params: LanguageServiceParams = {}; const settings = this.getLanguageSettings(); this._jsonLanguageService = getLanguageService(params); @@ -42,7 +51,7 @@ export class NativeWorkflowLanguageService extends WorkflowLanguageService { return NativeWorkflowSchema; } - public override parseWorkflowDocument(document: TextDocument): WorkflowDocument { + public override parseDocument(document: TextDocument): NativeWorkflowDocument { const jsonDocument = this._jsonLanguageService.parseJSONDocument(document); return new NativeWorkflowDocument(document, jsonDocument); } @@ -51,7 +60,7 @@ export class NativeWorkflowLanguageService extends WorkflowLanguageService { return this._jsonLanguageService.format(document, range, options); } - public override async doHover(workflowDocument: WorkflowDocument, position: Position): Promise { + public override async doHover(workflowDocument: NativeWorkflowDocument, position: Position): Promise { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; const hover = await this._jsonLanguageService.doHover( nativeWorkflowDocument.textDocument, @@ -62,7 +71,7 @@ export class NativeWorkflowLanguageService extends WorkflowLanguageService { } public override async doComplete( - workflowDocument: WorkflowDocument, + workflowDocument: NativeWorkflowDocument, position: Position ): Promise { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; @@ -74,7 +83,7 @@ export class NativeWorkflowLanguageService extends WorkflowLanguageService { return completionResult; } - protected override async doValidation(workflowDocument: WorkflowDocument): Promise { + protected override async doValidation(workflowDocument: NativeWorkflowDocument): Promise { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; const schemaValidationResults = await this._jsonLanguageService.doValidation( nativeWorkflowDocument.textDocument, diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index c49ba8f..287b059 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,5 +1,13 @@ +import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + TextDocument, + WorkflowDataType, + WorkflowDocument, +} from "@gxwf/server-common/src/languageTypes"; import { JSONDocument } from "vscode-json-languageservice"; -import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; +import { ToolState, isWorkflowInputType, type ParameterInputToolState } from "./utils"; /** * This class provides information about a Native workflow document structure. @@ -8,11 +16,105 @@ export class NativeWorkflowDocument extends WorkflowDocument { private _jsonDocument: JSONDocument; constructor(textDocument: TextDocument, jsonDocument: JSONDocument) { - super(textDocument, jsonDocument); + const parsedDocument: ParsedDocument = { + ...{ + root: jsonDocument.root as ASTNode, + getNodeFromOffset(offset: number) { + return jsonDocument.getNodeFromOffset(offset) as ASTNode | undefined; + }, + }, + internalDocument: jsonDocument, + }; + super(textDocument, parsedDocument); this._jsonDocument = jsonDocument; } public get jsonDocument(): JSONDocument { return this._jsonDocument; } + + public getWorkflowInputs(): GetWorkflowInputsResult { + const result: GetWorkflowInputsResult = { inputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const stepTypeNode = step.properties.find((property) => property.keyNode.value === "type"); + const stepTypeValue = String(stepTypeNode?.valueNode?.value); + if (isWorkflowInputType(stepTypeValue)) { + const labelNode = step.properties.find((property) => property.keyNode.value === "label"); + let labelValue = labelNode?.valueNode?.value; + const annotationNode = step.properties.find((property) => property.keyNode.value === "annotation"); + let annotationValue = annotationNode?.valueNode?.value; + if (!labelNode) { + const inputs = step.properties.find((property) => property.keyNode.value === "inputs"); + if (inputs?.valueNode && inputs.valueNode.type === "array") { + const input = inputs?.valueNode.items.at(0); + if (input && input.type === "object") { + labelValue = input.properties.find((p) => p.keyNode.value === "name")?.valueNode?.value ?? labelValue; + annotationValue = + input.properties.find((p) => p.keyNode.value === "description")?.valueNode?.value ?? annotationValue; + } + } + } + if (!labelValue) { + const nameNode = step.properties.find((property) => property.keyNode.value === "name"); + labelValue = nameNode?.valueNode?.value; + } + const toolStateNode = step.properties.find((property) => property.keyNode.value === "tool_state"); + const toolStateValue = JSON.parse( + toolStateNode?.valueNode?.value ? String(toolStateNode?.valueNode?.value) : "{}" + ); + result.inputs.push({ + name: String(labelValue), + doc: String(annotationValue ?? ""), + type: this.getInputType(stepTypeValue, toolStateValue), + }); + } + }); + return result; + } + + /** + * Returns the outputs of the workflow. + */ + public getWorkflowOutputs(): GetWorkflowOutputsResult { + const result: GetWorkflowOutputsResult = { outputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const workflowOutputsNode = step.properties.find((property) => property.keyNode.value === "workflow_outputs"); + if (workflowOutputsNode && workflowOutputsNode.valueNode?.type === "array") { + const workflowOutputs = workflowOutputsNode.valueNode.items; + workflowOutputs.forEach((workflowOutput) => { + if (workflowOutput.type !== "object") { + return; + } + const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); + let labelValue = labelNode?.valueNode?.value; + if (!labelValue) { + labelValue = workflowOutput.properties.find((property) => property.keyNode.value === "output_name") + ?.valueNode?.value; + } + const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); + const uuidValue = String(uuidNode?.valueNode?.value); + result.outputs.push({ + name: String(labelValue), + uuid: uuidValue, + }); + }); + } + }); + return result; + } + + private getInputType(typeName: string, toolStateValue: ToolState): WorkflowDataType { + switch (typeName) { + case "data_input": + return "data"; + case "data_collection_input": + return "collection"; + case "parameter_input": + return (toolStateValue as ParameterInputToolState).parameter_type as WorkflowDataType; + default: + return "data"; + } + } } diff --git a/server/gx-workflow-ls-native/src/node/server.ts b/server/gx-workflow-ls-native/src/node/server.ts index dfc4bdf..5f939b5 100644 --- a/server/gx-workflow-ls-native/src/node/server.ts +++ b/server/gx-workflow-ls-native/src/node/server.ts @@ -1,9 +1,8 @@ -import { createConnection } from "vscode-languageserver/node"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { NativeWorkflowLanguageService } from "../languageService"; +import { Connection, createConnection } from "vscode-languageserver/node"; +import { container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer, TYPES } from "@gxwf/server-common/src/languageTypes"; -const connection = createConnection(); +container.bind(TYPES.Connection).toConstantValue(createConnection()); -const languageService = new NativeWorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-native/src/utils.ts b/server/gx-workflow-ls-native/src/utils.ts new file mode 100644 index 0000000..d27bfb6 --- /dev/null +++ b/server/gx-workflow-ls-native/src/utils.ts @@ -0,0 +1,12 @@ +import { WorkflowDataType } from "@gxwf/server-common/src/languageTypes"; + +export function isWorkflowInputType(input: string): input is WorkflowDataType { + return ["data_input", "data_collection_input", "parameter_input"].includes(input); +} + +export interface ParameterInputToolState { + parameter_type: string; + optional: boolean; +} + +export type ToolState = ParameterInputToolState | unknown; diff --git a/server/gx-workflow-ls-native/tests/testHelpers.ts b/server/gx-workflow-ls-native/tests/testHelpers.ts index 768be97..b1eff01 100644 --- a/server/gx-workflow-ls-native/tests/testHelpers.ts +++ b/server/gx-workflow-ls-native/tests/testHelpers.ts @@ -1,6 +1,6 @@ import { ASTNode } from "@gxwf/server-common/src/ast/types"; -import { getLanguageService, JSONDocument } from "vscode-json-languageservice"; import { TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { getLanguageService, JSONDocument } from "vscode-json-languageservice"; import { NativeWorkflowDocument } from "../src/nativeWorkflowDocument"; export function toJsonDocument(contents: string): { textDoc: TextDocument; jsonDoc: JSONDocument } { @@ -14,7 +14,7 @@ export function toJsonDocument(contents: string): { textDoc: TextDocument; jsonD export function getJsonDocumentRoot(contents: string): ASTNode { const { jsonDoc } = toJsonDocument(contents); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return jsonDoc.root!; + return jsonDoc.root! as ASTNode; } export function createNativeWorkflowDocument(contents: string): NativeWorkflowDocument { diff --git a/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts b/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts index 8486b42..c480994 100644 --- a/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts +++ b/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts @@ -16,6 +16,8 @@ interface TestJsonWorkflows { withoutWorkflowOutputLabels: string; /** Workflow with 1 step. The step has 2 workflow_outputs with labels. */ withWorkflowOutputLabels: string; + /** Workflow with 6 steps. All steps are inputs with different types. */ + withOnlyInputs: string; }; } @@ -33,6 +35,7 @@ export class TestWorkflowProvider { path.join(TEST_DATA_PATH, "json", "validation", "test_wf_04.ga"), "utf-8" ), + withOnlyInputs: fs.readFileSync(path.join(TEST_DATA_PATH, "json", "validation", "test_wf_05.ga"), "utf-8"), }, }; diff --git a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts index 3aa6217..96c30bf 100644 --- a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts +++ b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts @@ -1,3 +1,4 @@ +import { WorkflowInput } from "@gxwf/server-common/src/languageTypes"; import { createNativeWorkflowDocument } from "../testHelpers"; import { TestWorkflowProvider } from "../testWorkflowProvider"; @@ -14,4 +15,103 @@ describe("NativeWorkflowDocument", () => { expect(stepNodes).toHaveLength(expectedNumSteps); }); }); + + describe("getWorkflowInputs", () => { + it.each<[string, WorkflowInput[]]>([ + ["", []], + [TestWorkflowProvider.workflows.validation.withoutSteps, []], + [ + TestWorkflowProvider.workflows.validation.withOneStep, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + [ + TestWorkflowProvider.workflows.validation.withThreeSteps, + [ + { + name: "WorkflowInput1", + doc: "input1 description", + type: "data", + }, + { + name: "WorkflowInput2", + doc: "", + type: "data", + }, + ], + ], + [ + TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + [ + TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + [ + TestWorkflowProvider.workflows.validation.withOnlyInputs, + [ + { doc: "", name: "Dataset Input", type: "data" }, + { doc: "", name: "Collection Input", type: "collection" }, + { doc: "", name: "Text Param", type: "text" }, + { doc: "", name: "Integer Param", type: "integer" }, + { doc: "", name: "Float Param", type: "float" }, + { doc: "", name: "Boolean Param", type: "boolean" }, + { doc: "", name: "Color Param", type: "color" }, + ], + ], + ])("returns the expected inputs", (wfContent: string, expectedInputs: WorkflowInput[]) => { + const document = createNativeWorkflowDocument(wfContent); + const result = document.getWorkflowInputs(); + expect(result.inputs).toHaveLength(expectedInputs.length); + expect(result.inputs).toEqual(expectedInputs); + }); + }); + + describe("getWorkflowOutputs", () => { + it.each([ + ["", 0], + [TestWorkflowProvider.workflows.validation.withoutSteps, 0], + [TestWorkflowProvider.workflows.validation.withOneStep, 0], + [TestWorkflowProvider.workflows.validation.withThreeSteps, 0], + [TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, 2], + [TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, 2], + ])("returns the expected number of outputs", (wf_content: string, expectedNumInputs: number) => { + const document = createNativeWorkflowDocument(wf_content); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(expectedNumInputs); + }); + + it("should return the expected information of the outputs with labels", () => { + const document = createNativeWorkflowDocument(TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(2); + expect(result.outputs).toEqual([ + { + name: "The first output", + uuid: "7f08baab-5426-427e-9640-85815d809261", + }, + { + name: "The second output", + uuid: "b58fce9c-e507-4714-abfc-739607e02eed", + }, + ]); + }); + it("should return the expected information of the outputs without labels", () => { + const document = createNativeWorkflowDocument( + TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels + ); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(2); + expect(result.outputs).toEqual([ + { + name: "output1", + uuid: "7f08baab-5426-427e-9640-85815d809261", + }, + { + name: "output2", + uuid: "b58fce9c-e507-4714-abfc-739607e02eed", + }, + ]); + }); + }); }); diff --git a/server/gx-workflow-ls-native/tsconfig.json b/server/gx-workflow-ls-native/tsconfig.json index a2c3b2d..198d885 100644 --- a/server/gx-workflow-ls-native/tsconfig.json +++ b/server/gx-workflow-ls-native/tsconfig.json @@ -3,12 +3,17 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, "sourceMap": true, "strict": true, - "rootDirs": ["src", "tests"] - }, - "include": ["src/**/*.ts", "tests/**/*.ts"] + "rootDirs": ["src", "tests"], + "paths": { + "@schemas/*": ["../../workflow-languages/schemas/*"] + } + } } diff --git a/server/package-lock.json b/server/package-lock.json index 17b7e5d..c6c1955 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -19,11 +19,17 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.8" } }, "gx-workflow-ls-native": { @@ -31,32 +37,450 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/@gxwf/server-common": { "resolved": "packages/server-common", "link": true }, + "node_modules/@gxwf/workflow-tests-language-service": { + "resolved": "packages/workflow-tests-language-service", + "link": true + }, "node_modules/@gxwf/yaml-language-service": { "resolved": "packages/yaml-language-service", "link": true }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" }, + "node_modules/@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "node_modules/@vscode/l10n": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "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 + }, "node_modules/gx-workflow-ls-format2": { "resolved": "gx-workflow-ls-format2", "link": true @@ -65,11 +489,223 @@ "resolved": "gx-workflow-ls-native", "link": true }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/vscode-json-languageservice": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.6.tgz", @@ -139,11 +775,34 @@ "license": "MIT", "dependencies": { "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.8" + } + }, + "packages/workflow-tests-language-service": { + "name": "@gxwf/workflow-tests-language-service", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@gxwf/server-common": "*", + "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.8" } }, "packages/yaml-language-service": { @@ -152,18 +811,169 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", "yaml": "^2.3.2" + }, + "devDependencies": { + "@types/jest": "^29.5.8" } } }, "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "@gxwf/server-common": { "version": "file:packages/server-common", "requires": { + "@types/jest": "^29.5.8", "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", @@ -171,31 +981,234 @@ "vscode-uri": "^3.0.7" } }, + "@gxwf/workflow-tests-language-service": { + "version": "file:packages/workflow-tests-language-service", + "requires": { + "@gxwf/server-common": "*", + "@gxwf/yaml-language-service": "*", + "@types/jest": "^29.5.8", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + } + }, "@gxwf/yaml-language-service": { "version": "file:packages/yaml-language-service", "requires": { "@gxwf/server-common": "*", + "@types/jest": "^29.5.8", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", "yaml": "^2.3.2" } }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" }, + "@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "@vscode/l10n": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "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 + }, "gx-workflow-ls-format2": { "version": "file:gx-workflow-ls-format2", "requires": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "@types/jest": "^29.5.8", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", @@ -206,7 +1219,11 @@ "version": "file:gx-workflow-ls-native", "requires": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", + "@types/jest": "^29.5.8", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", @@ -214,11 +1231,174 @@ "vscode-uri": "^3.0.7" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "vscode-json-languageservice": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.6.tgz", diff --git a/server/packages/server-common/package.json b/server/packages/server-common/package.json index fc506a3..2ba2876 100644 --- a/server/packages/server-common/package.json +++ b/server/packages/server-common/package.json @@ -7,11 +7,16 @@ "type": "module", "dependencies": { "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", + "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-languageserver": "^8.1.0", "vscode-uri": "^3.0.7" }, - "scripts": {} + "scripts": {}, + "devDependencies": { + "@types/jest": "^29.5.8" + } } diff --git a/server/packages/server-common/src/ast/types.ts b/server/packages/server-common/src/ast/types.ts index cec2efd..2c3ae8c 100644 --- a/server/packages/server-common/src/ast/types.ts +++ b/server/packages/server-common/src/ast/types.ts @@ -1,30 +1,62 @@ -import { - ArrayASTNode, - ASTNode, - BaseASTNode, - BooleanASTNode, - NullASTNode, - NumberASTNode, - ObjectASTNode, - PropertyASTNode, - StringASTNode, -} from "vscode-json-languageservice"; +export type ASTNode = + | ObjectASTNode + | PropertyASTNode + | ArrayASTNode + | StringASTNode + | NumberASTNode + | BooleanASTNode + | NullASTNode; -export { - ArrayASTNode, - ASTNode, - BaseASTNode, - BooleanASTNode, - NullASTNode, - NumberASTNode, - ObjectASTNode, - PropertyASTNode, - StringASTNode, -}; +export interface BaseASTNode { + readonly type: "object" | "array" | "property" | "string" | "number" | "boolean" | "null"; + readonly parent?: ASTNode; + readonly offset: number; + readonly length: number; + readonly children?: ASTNode[]; + readonly value?: string | boolean | number | null; + readonly internalNode: unknown; + getNodeFromOffsetEndInclusive(offset: number): ASTNode | undefined; +} +export interface ObjectASTNode extends BaseASTNode { + readonly type: "object"; + readonly properties: PropertyASTNode[]; + readonly children: ASTNode[]; +} +export interface PropertyASTNode extends BaseASTNode { + readonly type: "property"; + readonly keyNode: StringASTNode; + readonly valueNode?: ASTNode; + readonly colonOffset?: number; + readonly children: ASTNode[]; +} +export interface ArrayASTNode extends BaseASTNode { + readonly type: "array"; + readonly items: ASTNode[]; + readonly children: ASTNode[]; +} +export interface StringASTNode extends BaseASTNode { + readonly type: "string"; + readonly value: string; +} +export interface NumberASTNode extends BaseASTNode { + readonly type: "number"; + readonly value: number; + readonly isInteger: boolean; +} +export interface BooleanASTNode extends BaseASTNode { + readonly type: "boolean"; + readonly value: boolean; +} +export interface NullASTNode extends BaseASTNode { + readonly type: "null"; + readonly value: null; +} export interface ParsedDocument { root?: ASTNode; getNodeFromOffset(offset: number): ASTNode | undefined; + /** Exposed for compatibility with existing external logic. */ + internalDocument: unknown; } /** diff --git a/server/packages/server-common/src/configService.ts b/server/packages/server-common/src/configService.ts index eadc403..e22a096 100644 --- a/server/packages/server-common/src/configService.ts +++ b/server/packages/server-common/src/configService.ts @@ -1,9 +1,11 @@ +import { inject, injectable } from "inversify"; import { ClientCapabilities, Connection, DidChangeConfigurationNotification, DidChangeConfigurationParams, } from "vscode-languageserver"; +import { TYPES } from "./languageTypes"; /** Represents all the available settings of the extension. */ interface ExtensionSettings { @@ -41,20 +43,27 @@ let globalSettings: ExtensionSettings = defaultSettings; // Cache the settings of all open documents const documentSettingsCache: Map = new Map(); -export class ConfigService { +export interface ConfigService { + readonly connection: Connection; + initialize(capabilities: ClientCapabilities, onConfigurationChanged: () => void): void; + getDocumentSettings(uri: string): Promise; + onDocumentClose(uri: string): void; +} + +@injectable() +export class ConfigServiceImpl implements ConfigService { protected hasConfigurationCapability = false; + private onConfigurationChanged: () => void = () => { + return; + }; - constructor( - public readonly connection: Connection, - private readonly onConfigurationChanged: () => void = () => { - return; - } - ) { + constructor(@inject(TYPES.Connection) public readonly connection: Connection) { this.connection.onInitialized(() => this.onInitialized()); this.connection.onDidChangeConfiguration((params) => this.onDidChangeConfiguration(params)); } - public initialize(capabilities: ClientCapabilities): void { + public initialize(capabilities: ClientCapabilities, onConfigurationChanged: () => void): void { + this.onConfigurationChanged = onConfigurationChanged; this.hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration); } diff --git a/server/packages/server-common/src/index.ts b/server/packages/server-common/src/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/packages/server-common/src/inversify.config.ts b/server/packages/server-common/src/inversify.config.ts new file mode 100644 index 0000000..fce9f36 --- /dev/null +++ b/server/packages/server-common/src/inversify.config.ts @@ -0,0 +1,12 @@ +import { Container } from "inversify"; +import { ConfigService, ConfigServiceImpl } from "./configService"; +import { DocumentsCache, TYPES, WorkflowDataProvider } from "./languageTypes"; +import { DocumentsCacheImpl } from "./models/documentsCache"; +import { WorkflowDataProviderImpl } from "./providers/workflowDataProvider"; + +const container = new Container(); +container.bind(TYPES.ConfigService).to(ConfigServiceImpl).inSingletonScope(); +container.bind(TYPES.DocumentsCache).to(DocumentsCacheImpl).inSingletonScope(); +container.bind(TYPES.WorkflowDataProvider).to(WorkflowDataProviderImpl).inSingletonScope(); + +export { container }; diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 09c8fe9..fd3b3cd 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -1,99 +1,128 @@ +import "reflect-metadata"; import { - Range, - Position, - DocumentUri, - MarkupContent, - MarkupKind, + CodeAction, + CodeActionContext, + CodeActionKind, Color, ColorInformation, ColorPresentation, - FoldingRange, - FoldingRangeKind, - SelectionRange, - Diagnostic, - DiagnosticSeverity, + Command, CompletionItem, CompletionItemKind, - CompletionList, CompletionItemTag, - InsertTextFormat, - SymbolInformation, - SymbolKind, - DocumentSymbol, - Location, - Hover, - MarkedString, - FormattingOptions as LSPFormattingOptions, + CompletionList, DefinitionLink, - CodeActionContext, - Command, - CodeAction, + Diagnostic, + DiagnosticSeverity, DocumentHighlight, + DocumentHighlightKind, DocumentLink, - WorkspaceEdit, - TextEdit, - CodeActionKind, + DocumentSymbol, + DocumentUri, + FoldingRange, + FoldingRangeKind, + Hover, + InsertTextFormat, + FormattingOptions as LSPFormattingOptions, + Location, + MarkedString, + MarkupContent, + MarkupKind, + Position, + Range, + SelectionRange, + SymbolInformation, + SymbolKind, TextDocumentEdit, + TextEdit, VersionedTextDocumentIdentifier, - DocumentHighlightKind, + WorkspaceEdit, } from "vscode-languageserver-types"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { injectable, unmanaged } from "inversify"; import { Connection, DocumentFormattingParams, DocumentRangeFormattingParams, - HoverParams, DocumentSymbolParams, + HoverParams, } from "vscode-languageserver/browser"; +import { URI } from "vscode-uri"; +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, + WorkflowDataType, + WorkflowInput, + WorkflowOutput, +} from "../../../../shared/src/requestsDefinitions"; +import { ASTNodeManager } from "./ast/nodeManager"; +import { ConfigService } from "./configService"; import { WorkflowDocument } from "./models/workflowDocument"; -import { WorkflowDocuments } from "./models/workflowDocuments"; -import { GalaxyWorkflowLanguageServer } from "./server"; +import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; export { - TextDocument, - Range, - Position, - DocumentUri, - MarkupContent, - MarkupKind, + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + CodeAction, + CodeActionContext, + CodeActionKind, Color, ColorInformation, ColorPresentation, - FoldingRange, - FoldingRangeKind, - SelectionRange, - Diagnostic, - DiagnosticSeverity, + Command, CompletionItem, CompletionItemKind, - CompletionList, CompletionItemTag, - InsertTextFormat, + CompletionList, DefinitionLink, - SymbolInformation, - SymbolKind, + Diagnostic, + DiagnosticSeverity, + DocumentFormattingParams, + DocumentHighlight, + DocumentHighlightKind, + DocumentLink, + DocumentRangeFormattingParams, DocumentSymbol, - Location, + DocumentSymbolParams, + DocumentUri, + FoldingRange, + FoldingRangeKind, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, Hover, HoverParams, + InsertTextFormat, + LSRequestIdentifiers, + Location, MarkedString, - CodeActionContext, - Command, - CodeAction, - DocumentHighlight, - DocumentLink, - WorkspaceEdit, - TextEdit, - CodeActionKind, + MarkupContent, + MarkupKind, + Position, + Range, + SelectionRange, + SymbolInformation, + SymbolKind, + TargetWorkflowDocumentParams, + TextDocument, TextDocumentEdit, + TextEdit, VersionedTextDocumentIdentifier, - DocumentHighlightKind, - DocumentFormattingParams, - DocumentRangeFormattingParams, + WorkflowDataType, WorkflowDocument, - DocumentSymbolParams, + WorkflowInput, + WorkflowOutput, + WorkflowTestsDocument, + WorkspaceEdit, }; export interface FormattingOptions extends LSPFormattingOptions { @@ -103,10 +132,10 @@ export interface FormattingOptions extends LSPFormattingOptions { export interface HoverContentContributor { /** * Gets the contents that will be contributed to a new section of the Hover message - * @param workflowDocument The workflow document + * @param documentContext The document context * @param position The hover position */ - onHoverContent(workflowDocument: WorkflowDocument, position: Position): string; + onHoverContent(documentContext: DocumentContext, position: Position): string; } /** @@ -116,9 +145,9 @@ export interface ValidationRule { /** * Validates the given workflow document and provides diagnostics according * to this rule. - * @param workflowDocument The workflow document + * @param documentContext The workflow document */ - validate(workflowDocument: WorkflowDocument): Promise; + validate(documentContext: DocumentContext): Promise; } /** @@ -140,48 +169,107 @@ export interface WorkflowValidator { doValidation(workflowDocument: WorkflowDocument): Promise; } +/** + * Provides information about a processed text document. + */ +export interface DocumentContext { + languageId: string; + uri: URI; + textDocument: TextDocument; + nodeManager: ASTNodeManager; + internalDocument: unknown; +} + +export interface LanguageService { + readonly languageId: string; + + parseDocument(document: TextDocument): T; + format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; + doHover(documentContext: T, position: Position): Promise; + doComplete(documentContext: T, position: Position): Promise; + + /** + * Validates the document and reports all the diagnostics found. + * An optional validation profile can be used to provide additional custom diagnostics. + */ + validate(documentContext: T, useProfile?: ValidationProfile): Promise; + + setServer(server: GalaxyWorkflowLanguageServer): void; +} + /** * Abstract service defining the base functionality that a workflow language must * implement to provide assistance for workflow documents editing. */ -export abstract class WorkflowLanguageService { +@injectable() +export abstract class LanguageServiceBase implements LanguageService { + constructor(@unmanaged() public readonly languageId: string) {} + + protected server?: GalaxyWorkflowLanguageServer; + + public abstract parseDocument(document: TextDocument): T; public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; - public abstract parseWorkflowDocument(document: TextDocument): WorkflowDocument; - public abstract doHover(workflowDocument: WorkflowDocument, position: Position): Promise; - public abstract doComplete(workflowDocument: WorkflowDocument, position: Position): Promise; + public abstract doHover(documentContext: T, position: Position): Promise; + public abstract doComplete(documentContext: T, position: Position): Promise; - /** Performs basic syntax and semantic validation based on the workflow schema. */ - protected abstract doValidation(workflowDocument: WorkflowDocument): Promise; + /** Performs basic syntax and semantic validation based on the document schema. */ + protected abstract doValidation(documentContext: T): Promise; /** * Validates the document and reports all the diagnostics found. * An optional validation profile can be used to provide additional custom diagnostics. */ - public async validate( - workflowDocument: WorkflowDocument, - useProfile: ValidationProfile | null = null - ): Promise { - const diagnostics = await this.doValidation(workflowDocument); + public async validate(documentContext: T, useProfile?: ValidationProfile): Promise { + const diagnostics = await this.doValidation(documentContext); if (useProfile) { useProfile.rules.forEach(async (validationRule) => { - const contributedDiagnostics = await validationRule.validate(workflowDocument); + const contributedDiagnostics = await validationRule.validate(documentContext); diagnostics.push(...contributedDiagnostics); }); } return diagnostics; } -} -export abstract class ServerContext { - protected connection: Connection; - protected workflowDocuments: WorkflowDocuments; - protected languageService: WorkflowLanguageService; - protected server: GalaxyWorkflowLanguageServer; - - constructor(server: GalaxyWorkflowLanguageServer) { + public setServer(server: GalaxyWorkflowLanguageServer): void { this.server = server; - this.workflowDocuments = server.workflowDocuments; - this.languageService = server.languageService; - this.connection = server.connection; } } + +export interface WorkflowLanguageService extends LanguageService {} +export interface WorkflowTestsLanguageService extends LanguageService {} + +export interface GalaxyWorkflowLanguageServer { + connection: Connection; + documentsCache: DocumentsCache; + configService: ConfigService; + workflowDataProvider: WorkflowDataProvider; + start(): void; + getLanguageServiceById(languageId: string): LanguageService; +} + +export interface DocumentsCache { + get(documentUri: string): DocumentContext | undefined; + all(): DocumentContext[]; + addOrReplaceDocument(documentContext: DocumentContext): void; + removeDocument(documentUri: string): void; + dispose(): void; + + get schemesToSkip(): string[]; +} + +export interface WorkflowDataProvider { + getWorkflowInputs(workflowDocumentUri: string): Promise; + getWorkflowOutputs(workflowDocumentUri: string): Promise; +} + +const TYPES = { + DocumentsCache: Symbol.for("DocumentsCache"), + ConfigService: Symbol.for("ConfigService"), + Connection: Symbol.for("Connection"), + WorkflowLanguageService: Symbol.for("WorkflowLanguageService"), + WorkflowTestsLanguageService: Symbol.for("WorkflowTestsLanguageService"), + GalaxyWorkflowLanguageServer: Symbol.for("GalaxyWorkflowLanguageServer"), + WorkflowDataProvider: Symbol.for("WorkflowDataProvider"), +}; + +export { TYPES }; diff --git a/server/packages/server-common/src/models/document.ts b/server/packages/server-common/src/models/document.ts new file mode 100644 index 0000000..e9e2862 --- /dev/null +++ b/server/packages/server-common/src/models/document.ts @@ -0,0 +1,28 @@ +import { URI } from "vscode-uri"; +import { ASTNodeManager } from "../ast/nodeManager"; +import { ParsedDocument } from "../ast/types"; +import { DocumentContext, TextDocument } from "../languageTypes"; + +/** + * This class contains basic common handling logic for any kind of known document. + */ +export abstract class DocumentBase implements DocumentContext { + public readonly uri: URI; + public readonly nodeManager: ASTNodeManager; + + constructor( + public readonly textDocument: TextDocument, + protected readonly parsedDocument: ParsedDocument + ) { + this.nodeManager = new ASTNodeManager(textDocument, parsedDocument); + this.uri = URI.parse(textDocument.uri); + } + + public get languageId(): string { + return this.textDocument.languageId; + } + + public get internalDocument(): unknown { + return this.parsedDocument.internalDocument; + } +} diff --git a/server/packages/server-common/src/models/documentsCache.ts b/server/packages/server-common/src/models/documentsCache.ts new file mode 100644 index 0000000..9071906 --- /dev/null +++ b/server/packages/server-common/src/models/documentsCache.ts @@ -0,0 +1,49 @@ +import { injectable } from "inversify"; +import { DocumentContext, DocumentsCache } from "../languageTypes"; + +@injectable() +export class DocumentsCacheImpl implements DocumentsCache { + private cache: Map; + + /** + * Document URI schemes that represent temporal or readonly documents + * that should not be cached. + */ + private static schemesToSkip = ["temp", "galaxy-clean-workflow"]; + + constructor() { + this.cache = new Map(); + } + + get schemesToSkip(): string[] { + return DocumentsCacheImpl.schemesToSkip; + } + + public get(documentUri: string): DocumentContext | undefined { + return this.cache.get(documentUri); + } + + public all(): DocumentContext[] { + return Array.from(this.cache.values()); + } + + public addOrReplaceDocument(documentContext: DocumentContext): void { + if (this.schemesToSkip.includes(documentContext.uri.scheme)) { + return; + } + this.cache.set(documentContext.uri.toString(), documentContext); + // console.debug("Registering: ", document.uri.toString()); + // console.debug("Files registered: ", this.cache.size); + } + + public removeDocument(documentUri: string): void { + this.cache.delete(documentUri); + // console.debug("Un-registering: ", documentUri); + // console.debug("Files registered: ", this.cache.size); + } + + public dispose(): void { + this.cache.clear(); + //console.debug("Documents cache cleared"); + } +} diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index ab595b3..3060676 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,34 +1,17 @@ -import { TextDocument } from "../languageTypes"; -import { URI } from "vscode-uri"; -import { ParsedDocument } from "../ast/types"; -import { ASTNodeManager } from "../ast/nodeManager"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../languageTypes"; +import { DocumentBase } from "./document"; /** - * This class contains information about workflow semantics. + * This class abstracts the common logic of workflow documents. */ -export abstract class WorkflowDocument { - protected _textDocument: TextDocument; - protected _documentUri: URI; - protected _parsedDocument: ParsedDocument; - protected _nodeManager: ASTNodeManager; +export abstract class WorkflowDocument extends DocumentBase { + /** + * Returns the inputs of the workflow. + */ + public abstract getWorkflowInputs(): GetWorkflowInputsResult; - constructor(textDocument: TextDocument, parsedDocument: ParsedDocument) { - this._textDocument = textDocument; - this._parsedDocument = parsedDocument; - this._nodeManager = new ASTNodeManager(textDocument, parsedDocument); - this._documentUri = URI.parse(this._textDocument.uri); - } - - public get uri(): URI { - return this._documentUri; - } - - public get textDocument(): TextDocument { - return this._textDocument; - } - - /** Abstract Syntax Tree Node Manager associated with this document. */ - public get nodeManager(): ASTNodeManager { - return this._nodeManager; - } + /** + * Returns the outputs of the workflow. + */ + public abstract getWorkflowOutputs(): GetWorkflowOutputsResult; } diff --git a/server/packages/server-common/src/models/workflowDocuments.ts b/server/packages/server-common/src/models/workflowDocuments.ts deleted file mode 100644 index 3f3f3e0..0000000 --- a/server/packages/server-common/src/models/workflowDocuments.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { WorkflowDocument } from "./workflowDocument"; - -export class WorkflowDocuments { - private _documentsCache: Map; - - /** - * Workflow document URI schemes that represent temporal or readonly documents. - */ - public static schemesToSkip = ["temp", "galaxy-clean-workflow"]; - - constructor() { - this._documentsCache = new Map(); - } - - public get(documentUri: string): WorkflowDocument | undefined { - return this._documentsCache.get(documentUri); - } - - public all(): WorkflowDocument[] { - return Array.from(this._documentsCache.values()); - } - - public addOrReplaceWorkflowDocument(document: WorkflowDocument): void { - if (WorkflowDocuments.schemesToSkip.includes(document.uri.scheme)) { - return; - } - this._documentsCache.set(document.uri.toString(), document); - // console.debug("Registering: ", document.uri.toString()); - // console.debug("workflow files registered: ", this._documentsCache.size); - } - - public removeWorkflowDocument(documentUri: string): void { - this._documentsCache.delete(documentUri); - // console.debug("Un-registering: ", documentUri); - // console.debug("workflow files registered: ", this._documentsCache.size); - } - - public dispose(): void { - this._documentsCache.clear(); - //console.debug("workflow document cache cleared"); - } -} diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts new file mode 100644 index 0000000..aad76c1 --- /dev/null +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -0,0 +1,25 @@ +import { WorkflowDataProvider, WorkflowInput, WorkflowOutput } from "../languageTypes"; +import { DocumentBase } from "./document"; + +/** + * This class contains information about a document containing workflow tests. + */ +export abstract class WorkflowTestsDocument extends DocumentBase { + protected abstract readonly workflowDataProvider?: WorkflowDataProvider; + + /** + * Returns the inputs of the associated workflow if available or an empty array otherwise. + */ + public async getWorkflowInputs(): Promise { + const result = await this.workflowDataProvider?.getWorkflowInputs(this.textDocument.uri); + return result?.inputs ?? []; + } + + /** + * Returns the outputs of the associated workflow if available or an empty array otherwise. + */ + public async getWorkflowOutputs(): Promise { + const result = await this.workflowDataProvider?.getWorkflowOutputs(this.textDocument.uri); + return result?.outputs ?? []; + } +} diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index a53c054..0bca9a4 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -1,6 +1,6 @@ import { CompletionList, CompletionParams } from "vscode-languageserver"; -import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; export class CompletionProvider extends Provider { public static register(server: GalaxyWorkflowLanguageServer): CompletionProvider { @@ -9,12 +9,14 @@ export class CompletionProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onCompletion(async (params) => this.onCompletion(params)); + this.server.connection.onCompletion(async (params) => this.onCompletion(params)); } + private async onCompletion(params: CompletionParams): Promise { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); - if (workflowDocument) { - const result = await this.languageService.doComplete(workflowDocument, params.position); + const documentContext = this.server.documentsCache.get(params.textDocument.uri); + if (documentContext) { + const languageService = this.server.getLanguageServiceById(documentContext.languageId); + const result = await languageService.doComplete(documentContext, params.position); return result; } return null; diff --git a/server/packages/server-common/src/providers/formattingProvider.ts b/server/packages/server-common/src/providers/formattingProvider.ts index 1966c2f..2f4de4d 100644 --- a/server/packages/server-common/src/providers/formattingProvider.ts +++ b/server/packages/server-common/src/providers/formattingProvider.ts @@ -6,8 +6,8 @@ import { Range, DocumentFormattingParams, DocumentRangeFormattingParams, + GalaxyWorkflowLanguageServer, } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; export class FormattingProvider extends Provider { @@ -17,8 +17,8 @@ export class FormattingProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onDocumentFormatting((params) => this.onDocumentFormatting(params)); - this.connection.onDocumentRangeFormatting((params) => this.onDocumentRangeFormatting(params)); + this.server.connection.onDocumentFormatting((params) => this.onDocumentFormatting(params)); + this.server.connection.onDocumentRangeFormatting((params) => this.onDocumentRangeFormatting(params)); } public onDocumentFormatting(params: DocumentFormattingParams): TextEdit[] { @@ -30,11 +30,12 @@ export class FormattingProvider extends Provider { } private onFormat(documentUri: string, range: Range | undefined, options: FormattingOptions): TextEdit[] { - const workflowDocument = this.workflowDocuments.get(documentUri); - if (workflowDocument) { - const edits = this.languageService.format( - workflowDocument.textDocument, - range ?? this.getFullRange(workflowDocument.textDocument), + const documentContext = this.server.documentsCache.get(documentUri); + if (documentContext) { + const languageService = this.server.getLanguageServiceById(documentContext.languageId); + const edits = languageService.format( + documentContext.textDocument, + range ?? this.getFullRange(documentContext.textDocument), options ); return edits; diff --git a/server/packages/server-common/src/providers/hover/hoverProvider.ts b/server/packages/server-common/src/providers/hover/hoverProvider.ts index 094b924..4e30e43 100644 --- a/server/packages/server-common/src/providers/hover/hoverProvider.ts +++ b/server/packages/server-common/src/providers/hover/hoverProvider.ts @@ -1,5 +1,11 @@ -import { Hover, HoverParams, MarkupKind, MarkupContent, HoverContentContributor } from "../../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../../server"; +import { + Hover, + HoverParams, + MarkupKind, + MarkupContent, + HoverContentContributor, + GalaxyWorkflowLanguageServer, +} from "../../languageTypes"; import { Provider } from "../provider"; export class HoverProvider extends Provider { @@ -15,15 +21,16 @@ export class HoverProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer, contributors?: HoverContentContributor[]) { super(server); this.contributors = contributors ?? []; - this.connection.onHover((params) => this.onHover(params)); + this.server.connection.onHover((params) => this.onHover(params)); } private async onHover(params: HoverParams): Promise { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); - if (!workflowDocument) { + const documentContext = this.server.documentsCache.get(params.textDocument.uri); + if (!documentContext) { return null; } - const hover = await this.languageService.doHover(workflowDocument, params.position); + const languageService = this.server.getLanguageServiceById(documentContext.languageId); + const hover = await languageService.doHover(documentContext, params.position); if (!hover) { return null; } @@ -35,7 +42,7 @@ export class HoverProvider extends Provider { : `${hover.contents}`, ]; this.contributors.forEach((contentContributor) => { - const contributedContent = contentContributor.onHoverContent(workflowDocument, params.position); + const contributedContent = contentContributor.onHoverContent(documentContext, params.position); contentSections.push(contributedContent); }); this.setHoverContentSections(hover, contentSections); diff --git a/server/packages/server-common/src/providers/provider.ts b/server/packages/server-common/src/providers/provider.ts index 72064e2..4125c3f 100644 --- a/server/packages/server-common/src/providers/provider.ts +++ b/server/packages/server-common/src/providers/provider.ts @@ -1,8 +1,5 @@ -import { ServerContext } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; -export abstract class Provider extends ServerContext { - constructor(server: GalaxyWorkflowLanguageServer) { - super(server); - } +export abstract class Provider { + constructor(public server: GalaxyWorkflowLanguageServer) {} } diff --git a/server/packages/server-common/src/providers/symbolsProvider.ts b/server/packages/server-common/src/providers/symbolsProvider.ts index 1d1f5a1..bec53c7 100644 --- a/server/packages/server-common/src/providers/symbolsProvider.ts +++ b/server/packages/server-common/src/providers/symbolsProvider.ts @@ -1,6 +1,11 @@ import { ASTNode, ObjectASTNode, PropertyASTNode } from "../ast/types"; -import { DocumentSymbolParams, DocumentSymbol, SymbolKind, WorkflowDocument } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { + DocumentSymbolParams, + DocumentSymbol, + SymbolKind, + DocumentContext, + GalaxyWorkflowLanguageServer, +} from "../languageTypes"; import { Provider } from "./provider"; const IGNORE_SYMBOL_NAMES = new Set(["a_galaxy_workflow", "position", "uuid", "errors", "format-version", "version"]); @@ -12,20 +17,20 @@ export class SymbolsProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onDocumentSymbol((params) => this.onDocumentSymbol(params)); + this.server.connection.onDocumentSymbol((params) => this.onDocumentSymbol(params)); } public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); - if (workflowDocument) { - const symbols = this.getSymbols(workflowDocument); + const documentContext = this.server.documentsCache.get(params.textDocument.uri); + if (documentContext) { + const symbols = this.getSymbols(documentContext); return symbols; } return []; } - private getSymbols(workflowDocument: WorkflowDocument): DocumentSymbol[] { - const root = workflowDocument.nodeManager.root; + private getSymbols(documentContext: DocumentContext): DocumentSymbol[] { + const root = documentContext.nodeManager.root; if (!root) { return []; } @@ -41,7 +46,7 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = workflowDocument.nodeManager.getNodeRange(node); + const range = documentContext.nodeManager.getNodeRange(node); const selectionRange = range; const symbol = { name, kind: this.getSymbolKind(node.type), range, selectionRange, children: [] }; result.push(symbol); @@ -63,8 +68,8 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = workflowDocument.nodeManager.getNodeRange(property); - const selectionRange = workflowDocument.nodeManager.getNodeRange(property.keyNode); + const range = documentContext.nodeManager.getNodeRange(property); + const selectionRange = documentContext.nodeManager.getNodeRange(property.keyNode); const children: DocumentSymbol[] = []; const symbol: DocumentSymbol = { name: name, diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts new file mode 100644 index 0000000..0205b9a --- /dev/null +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -0,0 +1,72 @@ +import { inject, injectable } from "inversify"; +import { Connection } from "vscode-languageserver"; +import { + DocumentsCache, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TYPES, + TargetWorkflowDocumentParams, + WorkflowDataProvider, + WorkflowDocument, +} from "../languageTypes"; + +@injectable() +export class WorkflowDataProviderImpl implements WorkflowDataProvider { + constructor( + @inject(TYPES.Connection) public readonly connection: Connection, + @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache + ) { + // Register the request handler for getting workflow inputs + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, (params: TargetWorkflowDocumentParams) => { + // if we receive a request to get workflow inputs, we can expect that the workflow document is in the cache + // because the client should have opened it before sending the request. + const workflowDocument = this.getWorkflowDocument(params.uri); + return workflowDocument ? workflowDocument.getWorkflowInputs() : { inputs: [] }; + }); + + // Register the request handler for getting workflow outputs + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, (params: TargetWorkflowDocumentParams) => { + // if we receive a request to get workflow outputs, we can expect that the workflow document is in the cache + // because the client should have opened it before sending the request. + const workflowDocument = this.getWorkflowDocument(params.uri); + return workflowDocument ? workflowDocument.getWorkflowOutputs() : { outputs: [] }; + }); + } + + /** + * Returns the inputs of the associated workflow given the URI of the workflow document or the associated test document. + * @param workflowDocumentUri The URI of the workflow document or the associated test document. + * @returns The inputs of the associated workflow. + */ + public async getWorkflowInputs(workflowDocumentUri: string): Promise { + const params: TargetWorkflowDocumentParams = { + uri: workflowDocumentUri.toString(), + }; + // The URI could be of the associated test document. Since we don't know which kind of workflow document + // it is (.ga or format2), we need to ask the client to get the workflow inputs. + // The client will then delegate the request to the appropriate language server after making sure + // that the workflow document is in the cache by opening it. + return this.connection.sendRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, params); + } + + /** + * Returns the outputs of the associated workflow given the URI of the workflow document or the associated test document. + * @param workflowDocumentUri The URI of the workflow document or the associated test document. + * @returns The outputs of the associated workflow. + */ + public async getWorkflowOutputs(workflowDocumentUri: string): Promise { + const params: TargetWorkflowDocumentParams = { + uri: workflowDocumentUri.toString(), + }; + // The URI could be of the associated test document. Since we don't know which kind of workflow document + // it is (.ga or format2), we need to ask the client to get the workflow outputs. + // The client will then delegate the request to the appropriate language server after making sure + // that the workflow document is in the cache by opening it. + return this.connection.sendRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, params); + } + + private getWorkflowDocument(uri: string): WorkflowDocument | undefined { + return this.documentsCache.get(uri) as WorkflowDocument; + } +} diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index e0722fd..a52aac4 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -6,30 +6,46 @@ import { TextDocuments, WorkspaceFolder, } from "vscode-languageserver"; -import { TextDocument, WorkflowDocument, WorkflowLanguageService } from "./languageTypes"; -import { WorkflowDocuments } from "./models/workflowDocuments"; +import { + DocumentContext, + DocumentsCache, + GalaxyWorkflowLanguageServer, + LanguageService, + TYPES, + TextDocument, + WorkflowDataProvider, + WorkflowLanguageService, + WorkflowTestsLanguageService, +} from "./languageTypes"; import { FormattingProvider } from "./providers/formattingProvider"; import { HoverProvider } from "./providers/hover/hoverProvider"; import { SymbolsProvider } from "./providers/symbolsProvider"; import { CleanWorkflowService } from "./services/cleanWorkflow"; // import { DebugHoverContentContributor } from "./providers/hover/debugHoverContentContributor"; +import { inject, injectable } from "inversify"; import { ConfigService } from "./configService"; import { CompletionProvider } from "./providers/completionProvider"; import { ValidationProfiles } from "./providers/validation/profiles"; -export class GalaxyWorkflowLanguageServer { - public readonly languageService: WorkflowLanguageService; - public readonly configService: ConfigService; +@injectable() +export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageServer { public readonly documents = new TextDocuments(TextDocument); - public readonly workflowDocuments = new WorkflowDocuments(); protected workspaceFolders: WorkspaceFolder[] | null | undefined; + private languageServiceMapper: Map> = new Map(); constructor( - public readonly connection: Connection, - languageService: WorkflowLanguageService + @inject(TYPES.Connection) public readonly connection: Connection, + @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache, + @inject(TYPES.ConfigService) public readonly configService: ConfigService, + @inject(TYPES.WorkflowDataProvider) public readonly workflowDataProvider: WorkflowDataProvider, + @inject(TYPES.WorkflowLanguageService) public readonly workflowLanguageService: WorkflowLanguageService, + @inject(TYPES.WorkflowTestsLanguageService) workflowTestsLanguageService: WorkflowTestsLanguageService ) { - this.languageService = languageService; - this.configService = new ConfigService(connection, () => this.onConfigurationChanged()); + this.languageServiceMapper.set(workflowLanguageService.languageId, workflowLanguageService); + this.languageServiceMapper.set(workflowTestsLanguageService.languageId, workflowTestsLanguageService); + workflowLanguageService.setServer(this); + workflowTestsLanguageService.setServer(this); + // Track open, change and close text document events this.trackDocumentChanges(connection); @@ -46,8 +62,16 @@ export class GalaxyWorkflowLanguageServer { this.connection.listen(); } + public getLanguageServiceById(languageId: string): LanguageService { + const languageService = this.languageServiceMapper.get(languageId); + if (!languageService) { + throw new Error(`Language service not found for languageId: ${languageId}`); + } + return languageService; + } + private async initialize(params: InitializeParams): Promise { - this.configService.initialize(params.capabilities); + this.configService.initialize(params.capabilities, () => this.onConfigurationChanged()); this.workspaceFolders = params.workspaceFolders; const capabilities: ServerCapabilities = { @@ -88,35 +112,37 @@ export class GalaxyWorkflowLanguageServer { * An event that fires when a workflow document has been opened or the content changes. */ private onDidChangeContent(textDocument: TextDocument): void { - const workflowDocument = this.languageService.parseWorkflowDocument(textDocument); - this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); - this.validate(workflowDocument); + const languageService = this.getLanguageServiceById(textDocument.languageId); + const documentContext = languageService.parseDocument(textDocument); + this.documentsCache.addOrReplaceDocument(documentContext); + this.validateDocument(documentContext); } private onDidClose(textDocument: TextDocument): void { - this.workflowDocuments.removeWorkflowDocument(textDocument.uri); + this.documentsCache.removeDocument(textDocument.uri); this.configService.onDocumentClose(textDocument.uri); this.clearValidation(textDocument); } private onConfigurationChanged(): void { - this.workflowDocuments.all().forEach((workflowDocument) => { - this.validate(workflowDocument); + this.documentsCache.all().forEach((documentContext) => { + this.validateDocument(documentContext); }); } private cleanup(): void { - this.workflowDocuments.dispose(); + this.documentsCache.dispose(); } - private async validate(workflowDocument: WorkflowDocument): Promise { - if (WorkflowDocuments.schemesToSkip.includes(workflowDocument.uri.scheme)) { + private async validateDocument(documentContext: DocumentContext): Promise { + if (this.documentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { return; } - const settings = await this.configService.getDocumentSettings(workflowDocument.textDocument.uri); + const settings = await this.configService.getDocumentSettings(documentContext.textDocument.uri); const validationProfile = ValidationProfiles.get(settings.validation.profile); - this.languageService.validate(workflowDocument, validationProfile).then((diagnostics) => { - this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); + const languageService = this.getLanguageServiceById(documentContext.languageId); + languageService.validate(documentContext, validationProfile).then((diagnostics) => { + this.connection.sendDiagnostics({ uri: documentContext.textDocument.uri, diagnostics }); }); } diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index 1050960..1f94cb8 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -1,17 +1,17 @@ import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { WorkflowDocument } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; import { ServiceBase } from "."; + +import { ASTNode, PropertyASTNode } from "../ast/types"; import { CleanWorkflowContentsParams, - CleanWorkflowContentsRequest, CleanWorkflowContentsResult, CleanWorkflowDocumentParams, - CleanWorkflowDocumentRequest, CleanWorkflowDocumentResult, -} from "./requestsDefinitions"; -import { ASTNode, PropertyASTNode } from "../ast/types"; + GalaxyWorkflowLanguageServer, + LSRequestIdentifiers, + WorkflowDocument, +} from "../languageTypes"; /** * Service for handling workflow `cleaning` requests. @@ -30,16 +30,18 @@ export class CleanWorkflowService extends ServiceBase { } protected listenToRequests(): void { - this.connection.onRequest(CleanWorkflowContentsRequest.type, (params) => - this.onCleanWorkflowContentsRequest(params) + this.server.connection.onRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS, + (params: CleanWorkflowContentsParams) => this.onCleanWorkflowContentsRequest(params) ); - this.connection.onRequest(CleanWorkflowDocumentRequest.type, (params) => - this.onCleanWorkflowDocumentRequest(params) + this.server.connection.onRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT, + (params: CleanWorkflowDocumentParams) => this.onCleanWorkflowDocumentRequest(params) ); } /** - * Processes a `CleanWorkflowContentsRequest` by returning the `clean` contents + * Processes a `CLEAN_WORKFLOW_DOCUMENT` request by returning the `clean` contents * of a workflow document given the raw text contents of the workflow document. * @param params The request parameters containing the raw text contents of the workflow * @returns The `clean` contents of the workflow document @@ -48,7 +50,8 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowContentsParams ): Promise { const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); - const workflowDocument = this.languageService.parseWorkflowDocument(tempDocument); + const workflowLanguageService = this.server.getLanguageServiceById(tempDocument.languageId); + const workflowDocument = workflowLanguageService.parseDocument(tempDocument) as WorkflowDocument; if (workflowDocument) { return await this.cleanWorkflowContentsResult(workflowDocument); } @@ -65,10 +68,13 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowDocumentParams ): Promise { try { - const workflowDocument = this.workflowDocuments.get(params.uri); + const workflowDocument = this.server.documentsCache.get(params.uri); if (workflowDocument) { const settings = await this.server.configService.getDocumentSettings(workflowDocument.textDocument.uri); - const edits = this.getTextEditsToCleanWorkflow(workflowDocument, settings.cleaning.cleanableProperties); + const edits = this.getTextEditsToCleanWorkflow( + workflowDocument as WorkflowDocument, + settings.cleaning.cleanableProperties + ); const editParams: ApplyWorkspaceEditParams = { label: "Clean workflow", edit: { @@ -83,7 +89,7 @@ export class CleanWorkflowService extends ServiceBase { ], }, }; - this.connection.workspace.applyEdit(editParams); + this.server.connection.workspace.applyEdit(editParams); } return { error: "" }; } catch (error) { diff --git a/server/packages/server-common/src/services/index.ts b/server/packages/server-common/src/services/index.ts index 83bb32d..d657a44 100644 --- a/server/packages/server-common/src/services/index.ts +++ b/server/packages/server-common/src/services/index.ts @@ -1,9 +1,7 @@ -import { ServerContext } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; -export abstract class ServiceBase extends ServerContext { - constructor(server: GalaxyWorkflowLanguageServer) { - super(server); +export abstract class ServiceBase { + constructor(public server: GalaxyWorkflowLanguageServer) { this.listenToRequests(); } diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts deleted file mode 100644 index 190d968..0000000 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -import { RequestType } from "vscode-languageserver"; - -// TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 - -export namespace LSRequestIdentifiers { - export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; - export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; -} - -export interface CleanWorkflowDocumentParams { - uri: string; -} - -export interface CleanWorkflowDocumentResult { - error: string; -} - -export interface CleanWorkflowContentsParams { - contents: string; -} - -export interface CleanWorkflowContentsResult { - contents: string; -} - -export namespace CleanWorkflowDocumentRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT - ); -} - -export namespace CleanWorkflowContentsRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS - ); -} diff --git a/server/packages/server-common/tests/testHelpers.ts b/server/packages/server-common/tests/testHelpers.ts index 0e56c61..b8239dc 100644 --- a/server/packages/server-common/tests/testHelpers.ts +++ b/server/packages/server-common/tests/testHelpers.ts @@ -1,6 +1,87 @@ import { ASTNode, PropertyASTNode } from "../src/ast/types"; +import { WorkflowDataProvider, WorkflowInput, WorkflowOutput } from "../src/languageTypes"; export function expectPropertyNodeToHaveKey(propertyNode: ASTNode | null, expectedPropertyKey: string): void { expect(propertyNode?.type).toBe("property"); expect((propertyNode as PropertyASTNode).keyNode.value).toBe(expectedPropertyKey); } + +/** + * Simulates the position of the cursor in the contents of a text document. + * @param template Represents the contents of a text document with a single character to be replaced. + * @param char Defaults to "$". The character to be replaced in the template. Its position will be used to simulate the position of the cursor. + * @returns The contents of the template string with the character removed and the position of the character. + */ +export function parseTemplate( + template: string, + char?: string +): { contents: string; position: { line: number; character: number } } { + if (!char) { + char = "$"; + } + let position = { line: 0, character: 0 }; + const contents = template.replace(char, ""); + + const lines = template.split("\n"); + for (let i = 0; i < lines.length; i++) { + const character = lines[i].indexOf(char); + if (character !== -1) { + position = { line: i, character }; + return { contents, position }; + } + } + + return { contents, position }; +} + +export const FAKE_DATASET_INPUT: WorkflowInput = { + name: "My fake dataset", + doc: "This is a simple dataset", + type: "data", +}; + +export const EXPECTED_WORKFLOW_INPUTS: WorkflowInput[] = [ + FAKE_DATASET_INPUT, + { + name: "Input dataset: fake", + doc: "This is a simple dataset with a colon in the name", + type: "File", + }, + { + name: "My fake collection", + doc: "This is a collection", + type: "collection", + }, +]; + +export const EXPECTED_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ + { + name: "My output", + uuid: "1234-5678-91011-1213", + }, + { + name: "My second output", + uuid: "1234-5678-91011-1214", + }, + { + name: "My third output: with colon", + uuid: "1234-5678-91011-1215", + }, +]; + +/** + * A fake implementation of the WorkflowDataProvider interface. + * Simulates a workflow with the expected inputs and outputs. + */ +export const FAKE_WORKFLOW_DATA_PROVIDER: WorkflowDataProvider = { + async getWorkflowInputs(_workflowDocumentUri: string) { + return { + inputs: EXPECTED_WORKFLOW_INPUTS, + }; + }, + async getWorkflowOutputs(_workflowDocumentUri: string) { + return { + outputs: EXPECTED_WORKFLOW_OUTPUTS, + }; + }, +}; diff --git a/server/packages/server-common/tsconfig.json b/server/packages/server-common/tsconfig.json index eae02d0..019c99f 100644 --- a/server/packages/server-common/tsconfig.json +++ b/server/packages/server-common/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, @@ -10,8 +13,11 @@ "strict": true, "composite": true, "declaration": true, - "rootDirs": ["src", "tests"], - "baseUrl": "." + "baseUrl": ".", + "rootDir": "../../../", + "paths": { + "@schemas/*": ["../../../workflow-languages/schemas/*"] + } }, - "include": ["src/**/*"] + "include": ["src/**/*", "../../../shared/**/*", "../../../workflow-languages/**/*"] } diff --git a/server/packages/workflow-tests-language-service/package.json b/server/packages/workflow-tests-language-service/package.json new file mode 100644 index 0000000..c1a1c8b --- /dev/null +++ b/server/packages/workflow-tests-language-service/package.json @@ -0,0 +1,23 @@ +{ + "name": "@gxwf/workflow-tests-language-service", + "version": "0.1.0", + "description": "Language service implementation for Galaxy workflow tests files. ", + "author": "davelopez", + "license": "MIT", + "dependencies": { + "@gxwf/server-common": "*", + "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + }, + "scripts": { + "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.8" + } +} diff --git a/server/packages/workflow-tests-language-service/src/document.ts b/server/packages/workflow-tests-language-service/src/document.ts new file mode 100644 index 0000000..c62688f --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/document.ts @@ -0,0 +1,18 @@ +import { TextDocument, WorkflowDataProvider, WorkflowTestsDocument } from "@gxwf/server-common/src/languageTypes"; +import { YAMLDocument } from "@gxwf/yaml-language-service/src"; + +/** + * This class represents (YAML) document containing tests definitions for a Galaxy workflow. + */ +export class GxWorkflowTestsDocument extends WorkflowTestsDocument { + protected readonly workflowDataProvider?: WorkflowDataProvider; + + constructor( + textDocument: TextDocument, + public readonly yamlDocument: YAMLDocument, + workflowDataProvider?: WorkflowDataProvider + ) { + super(textDocument, yamlDocument); + this.workflowDataProvider = workflowDataProvider; + } +} diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts new file mode 100644 index 0000000..b7e58eb --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -0,0 +1,36 @@ +import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { ContainerModule } from "inversify"; +import { GxWorkflowTestsLanguageServiceImpl } from "./languageService"; +import { JSONSchemaService, JSONSchemaServiceImpl } from "./schema/adapter"; +import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; +import { WorkflowTestsSchemaService, WorkflowTestsSchemaServiceImpl } from "./schema/service"; +import { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl } from "./services/completion"; +import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; +import { WorkflowTestsValidationService, WorkflowTestsValidationServiceImpl } from "./services/validation"; +import { TYPES } from "./types"; + +export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { + bind(TYPES.WorkflowTestsSchemaProvider) + .to(WorkflowTestsSchemaProviderImpl) + .inSingletonScope(); + + bind(TYPES.JSONSchemaService).to(JSONSchemaServiceImpl).inSingletonScope(); + + bind(TYPES.WorkflowTestsSchemaService) + .to(WorkflowTestsSchemaServiceImpl) + .inSingletonScope(); + + bind(TYPES.WorkflowTestsHoverService).to(WorkflowTestsHoverServiceImpl).inSingletonScope(); + + bind(TYPES.WorkflowTestsCompletionService) + .to(WorkflowTestsCompletionServiceImpl) + .inSingletonScope(); + + bind(TYPES.WorkflowTestsValidationService) + .to(WorkflowTestsValidationServiceImpl) + .inSingletonScope(); + + bind(COMMON_TYPES.WorkflowTestsLanguageService) + .to(GxWorkflowTestsLanguageServiceImpl) + .inSingletonScope(); +}); diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts new file mode 100644 index 0000000..98ffc4f --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -0,0 +1,63 @@ +import { + CompletionList, + Diagnostic, + FormattingOptions, + Hover, + LanguageServiceBase, + Position, + Range, + TextDocument, + TextEdit, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; +import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { inject, injectable } from "inversify"; +import { GxWorkflowTestsDocument } from "./document"; +import { WorkflowTestsCompletionService } from "./services/completion"; +import { WorkflowTestsHoverService } from "./services/hover"; +import { WorkflowTestsValidationService } from "./services/validation"; +import { TYPES } from "./types"; + +const LANGUAGE_ID = "gxwftests"; + +/** + * A custom implementation of the YAML Language Service to support language features + * for Galaxy workflow test files. + * It combines specific services to implement the language features. + */ +@injectable() +export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { + constructor( + @inject(YAML_TYPES.YAMLLanguageService) protected yamlLanguageService: YAMLLanguageService, + @inject(TYPES.WorkflowTestsHoverService) protected hoverService: WorkflowTestsHoverService, + @inject(TYPES.WorkflowTestsCompletionService) protected completionService: WorkflowTestsCompletionService, + @inject(TYPES.WorkflowTestsValidationService) protected validationService: WorkflowTestsValidationService + ) { + super(LANGUAGE_ID); + } + + public override parseDocument(document: TextDocument): GxWorkflowTestsDocument { + const yamlDocument = this.yamlLanguageService.parseYAMLDocument(document); + return new GxWorkflowTestsDocument(document, yamlDocument, this.server?.workflowDataProvider); + } + + public override format(document: TextDocument, _: Range, options: FormattingOptions): TextEdit[] { + return this.yamlLanguageService.doFormat(document, options); + } + + public override doHover(documentContext: WorkflowTestsDocument, position: Position): Promise { + return this.hoverService.doHover(documentContext, position); + } + + public override doComplete( + documentContext: WorkflowTestsDocument, + position: Position + ): Promise { + return this.completionService.doComplete(documentContext, position); + } + + protected override async doValidation(documentContext: WorkflowTestsDocument): Promise { + return this.validationService.doValidation(documentContext); + } +} diff --git a/server/packages/workflow-tests-language-service/src/schema/adapter.ts b/server/packages/workflow-tests-language-service/src/schema/adapter.ts new file mode 100644 index 0000000..9a512c7 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/adapter.ts @@ -0,0 +1,1377 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +import { + ASTNode, + ArrayASTNode, + NumberASTNode, + ObjectASTNode, + PropertyASTNode, + StringASTNode, +} from "@gxwf/server-common/src/ast/types"; +import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; +import { injectable } from "inversify"; +import { URI } from "vscode-uri"; +import { JSONSchema, JSONSchemaRef } from "./jsonSchema"; + +const YAML_SCHEMA_PREFIX = "yaml-schema: "; +export const YAML_SOURCE = "YAML"; + +/** + * Error codes used by diagnostics + */ +export enum ErrorCode { + Undefined = 0, + EnumValueMismatch = 1, + Deprecated = 2, + UnexpectedEndOfComment = 257, + UnexpectedEndOfString = 258, + UnexpectedEndOfNumber = 259, + InvalidUnicode = 260, + InvalidEscapeCharacter = 261, + InvalidCharacter = 262, + PropertyExpected = 513, + CommaExpected = 514, + ColonExpected = 515, + ValueExpected = 516, + CommaOrCloseBacketExpected = 517, + CommaOrCloseBraceExpected = 518, + TrailingComma = 519, + DuplicateKey = 520, + CommentNotPermitted = 521, + PropertyKeysMustBeDoublequoted = 528, + SchemaResolveError = 768, + SchemaUnsupportedFeature = 769, +} + +const propertyNotAllowedMessage = (property: string): string => `Property ${property} is not allowed.`; + +const formats = { + "color-hex": { + errorMessage: "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.", + pattern: /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/, + }, + "date-time": { + errorMessage: "String is not a RFC3339 date-time.", + pattern: + /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i, + }, + date: { + errorMessage: "String is not a RFC3339 date.", + pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/i, + }, + time: { + errorMessage: "String is not a RFC3339 time.", + pattern: /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i, + }, + email: { + errorMessage: "String is not an e-mail address.", + pattern: + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + }, + ipv4: { + errorMessage: "String does not match IPv4 format.", + pattern: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, + }, + ipv6: { + errorMessage: "String does not match IPv6 format.", + pattern: /^([0-9a-f]|:){1,4}(:([0-9a-f]{0,4})*){1,7}$/i, + }, +}; + +export interface IApplicableSchema { + node: ASTNode; + schema: JSONSchema; +} + +interface ISchemaCollector { + schemas: IApplicableSchema[]; + add(schema: IApplicableSchema): void; + merge(other: ISchemaCollector): void; + include(node: ASTNode): boolean; + newSub(): ISchemaCollector; +} + +class SchemaCollector implements ISchemaCollector { + schemas: IApplicableSchema[] = []; + constructor( + private focusOffset = -1, + private exclude: ASTNode | null = null + ) {} + add(schema: IApplicableSchema): void { + this.schemas.push(schema); + } + merge(other: ISchemaCollector): void { + this.schemas.push(...other.schemas); + } + include(node: ASTNode): boolean { + return (this.focusOffset === -1 || contains(node, this.focusOffset)) && node !== this.exclude; + } + newSub(): ISchemaCollector { + return new SchemaCollector(-1, this.exclude); + } +} + +class NoOpSchemaCollector implements ISchemaCollector { + private constructor() { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get schemas(): any[] { + return []; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + add(schema: IApplicableSchema): void { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + merge(other: ISchemaCollector): void { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + include(node: ASTNode): boolean { + return true; + } + newSub(): ISchemaCollector { + return this; + } + + static instance = new NoOpSchemaCollector(); +} + +export function contains(node: ASTNode, offset: number, includeRightBound = false): boolean { + return ( + (offset >= node.offset && offset <= node.offset + node.length) || + (includeRightBound && offset === node.offset + node.length) + ); +} + +export interface JSONSchemaService { + validate( + documentContext: DocumentContext, + schema: JSONSchema | undefined, + severity?: DiagnosticSeverity, + disableAdditionalProperties?: boolean + ): Diagnostic[] | undefined; + + getMatchingSchemas( + documentContext: DocumentContext, + schema: JSONSchema, + focusOffset?: number, + exclude?: ASTNode | null, + didCallFromAutoComplete?: boolean, + disableAdditionalProperties?: boolean + ): IApplicableSchema[]; +} + +@injectable() +export class JSONSchemaServiceImpl implements JSONSchemaService { + public validate( + documentContext: DocumentContext, + schema: JSONSchema | undefined, + severity: DiagnosticSeverity = DiagnosticSeverity.Warning, + disableAdditionalProperties = false + ): Diagnostic[] | undefined { + const root = documentContext.nodeManager.root!; + if (root && schema) { + const validationResult = new ValidationResult(); + validate(root, schema, schema, validationResult, NoOpSchemaCollector.instance, { + disableAdditionalProperties, + uri: documentContext.textDocument.uri, + callFromAutoComplete: false, + }); + return validationResult.problems.map((p) => { + const range = Range.create( + documentContext.textDocument.positionAt(p.location.offset), + documentContext.textDocument.positionAt(p.location.offset + p.location.length) + ); + return Diagnostic.create(range, p.message, p.severity ?? severity, p.code); + }); + } + return undefined; + } + + public getMatchingSchemas( + documentContext: DocumentContext, + schema: JSONSchema, + focusOffset = -1, + exclude: ASTNode | null = null, + didCallFromAutoComplete?: boolean, + disableAdditionalProperties = false + ): IApplicableSchema[] { + const root = documentContext.nodeManager.root!; + const matchingSchemas = new SchemaCollector(focusOffset, exclude); + if (root && schema) { + validate(root, schema, schema, new ValidationResult(), matchingSchemas, { + disableAdditionalProperties, + uri: documentContext.textDocument.uri, + callFromAutoComplete: didCallFromAutoComplete, + }); + } + return matchingSchemas.schemas; + } +} + +enum ProblemType { + missingRequiredPropWarning = "missingRequiredPropWarning", + typeMismatchWarning = "typeMismatchWarning", + constWarning = "constWarning", +} + +const ProblemTypeMessages: Record = { + [ProblemType.missingRequiredPropWarning]: 'Missing property "{0}".', + [ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".', + [ProblemType.constWarning]: "Value must be {0}.", +}; + +function getWarningMessage(problemType?: ProblemType, args?: string[]): string { + if (!problemType) { + throw new Error("Unknown problem type while getting warning message"); + } + if (!args) { + throw new Error("No arguments while getting warning message"); + } + return ProblemTypeMessages[problemType].replace("{0}", args.join(" | ")); +} + +interface IRange { + offset: number; + length: number; +} + +interface IProblem { + location: IRange; + severity: DiagnosticSeverity; + code?: ErrorCode; + message: string; + source: string; + problemType?: ProblemType; + problemArgs?: string[]; + schemaUri: string[]; + data?: Record; +} + +export function isArrayEqual(fst?: Array, snd?: Array): boolean { + if (!snd || !fst) { + return false; + } + if (snd.length !== fst.length) { + return false; + } + for (let index = fst.length - 1; index >= 0; index--) { + if (fst[index] !== snd[index]) { + return false; + } + } + return true; +} + +export class ValidationResult { + public problems: IProblem[]; + + public propertiesMatches: number; + public propertiesValueMatches: number; + public primaryValueMatches: number; + public enumValueMatch: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public enumValues: any[]; + + constructor() { + this.problems = []; + this.propertiesMatches = 0; + this.propertiesValueMatches = 0; + this.primaryValueMatches = 0; + this.enumValueMatch = false; + this.enumValues = []; + } + + public hasProblems(): boolean { + return !!this.problems.length; + } + + public mergeAll(validationResults: ValidationResult[]): void { + for (const validationResult of validationResults) { + this.merge(validationResult); + } + } + + public merge(validationResult: ValidationResult): void { + this.problems = this.problems.concat(validationResult.problems); + } + + public mergeEnumValues(validationResult: ValidationResult): void { + if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { + this.enumValues = this.enumValues.concat(validationResult.enumValues); + for (const error of this.problems) { + if (error.code === ErrorCode.EnumValueMismatch) { + error.message = `Value is not accepted. Valid values: ${[...new Set(this.enumValues)] + .map((v) => { + return JSON.stringify(v); + }) + .join(", ")}.`; + } + } + } + } + + /** + * Merge multiple warnings with same problemType together + * @param subValidationResult another possible result + */ + public mergeWarningGeneric(subValidationResult: ValidationResult, problemTypesToMerge: ProblemType[]): void { + if (this.problems?.length) { + for (const problemType of problemTypesToMerge) { + const bestResults = this.problems.filter((p) => p.problemType === problemType); + for (const bestResult of bestResults) { + const mergingResult = subValidationResult.problems?.find( + (p) => + p.problemType === problemType && + bestResult.location.offset === p.location.offset && + (problemType !== ProblemType.missingRequiredPropWarning || + isArrayEqual(p.problemArgs, bestResult.problemArgs)) // missingProp is merged only with same problemArg + ); + if (mergingResult) { + if (mergingResult.problemArgs?.length) { + mergingResult.problemArgs + .filter((p) => !bestResult.problemArgs?.includes(p)) + .forEach((p) => bestResult.problemArgs?.push(p)); + bestResult.message = getWarningMessage(bestResult.problemType, bestResult.problemArgs); + } + this.mergeSources(mergingResult, bestResult); + } + } + } + } + } + + public mergePropertyMatch(propertyValidationResult: ValidationResult): void { + this.merge(propertyValidationResult); + this.propertiesMatches++; + if ( + propertyValidationResult.enumValueMatch || + (!propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches) + ) { + this.propertiesValueMatches++; + } + if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues) { + this.primaryValueMatches++; + } + } + + private mergeSources(mergingResult: IProblem, bestResult: IProblem): void { + const mergingSource = mergingResult.source.replace(YAML_SCHEMA_PREFIX, ""); + if (!bestResult.source.includes(mergingSource)) { + bestResult.source = bestResult.source + " | " + mergingSource; + } + if (!bestResult.schemaUri.includes(mergingResult.schemaUri[0])) { + bestResult.schemaUri = bestResult.schemaUri.concat(mergingResult.schemaUri); + } + } + + public compareGeneric(other: ValidationResult): number { + const hasProblems = this.hasProblems(); + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + return this.propertiesMatches - other.propertiesMatches; + } + + public compareKubernetes(other: ValidationResult): number { + const hasProblems = this.hasProblems(); + if (this.propertiesMatches !== other.propertiesMatches) { + return this.propertiesMatches - other.propertiesMatches; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + return this.propertiesMatches - other.propertiesMatches; + } +} + +interface Options { + disableAdditionalProperties: boolean; + uri: string; + callFromAutoComplete?: boolean; +} + +export function asSchema(schema?: JSONSchemaRef): JSONSchema | undefined { + if (schema === undefined) { + return undefined; + } + + if (isBoolean(schema)) { + return schema ? {} : { not: {} }; + } + + if (typeof schema !== "object") { + // we need to report this case as JSONSchemaRef MUST be an Object or Boolean + console.warn(`Wrong schema: ${JSON.stringify(schema)}, it MUST be an Object or Boolean`); + schema = { + type: schema, + }; + } + if (schema.$ref) { + console.debug(`DEF ${schema.$ref}`); + } + return schema; +} + +function getSchemaSource(schema: JSONSchema): string { + let label: string | undefined = undefined; + if (schema) { + if (schema.title) { + label = schema.title; + return `${YAML_SCHEMA_PREFIX}${label}`; + } + } + + return YAML_SOURCE; +} + +function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string[] { + const uriString = schema.url ?? originalSchema.url; + return uriString ? [uriString] : []; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getNodeValue(node: ASTNode): any { + switch (node.type) { + case "array": + return node.children.map(getNodeValue); + case "object": { + const obj = Object.create(null); + for (let _i = 0, _a = node.children; _i < _a.length; _i++) { + const prop = _a[_i]; + const valueNode = prop.children && prop.children[1]; + if (valueNode) { + obj[prop.children[0].value as string] = getNodeValue(valueNode); + } + } + return obj; + } + case "null": + case "string": + case "number": + case "boolean": + return node.value; + default: + return undefined; + } +} + +function validate( + node: ASTNode, + schema: JSONSchema | undefined, + originalSchema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector, + options: Options + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const { callFromAutoComplete } = options; + if (!node) { + return; + } + + // schema should be an Object + if (typeof schema !== "object") { + return; + } + + if (!schema.url) { + schema.url = originalSchema.url; + } + + switch (node.type) { + case "object": + _validateObjectNode(node, schema, validationResult, matchingSchemas); + break; + case "array": + _validateArrayNode(node, schema, validationResult, matchingSchemas); + break; + case "string": + _validateStringNode(node, schema, validationResult); + break; + case "number": + _validateNumberNode(node, schema, validationResult); + break; + case "property": + return validate(node.valueNode!, schema, schema, validationResult, matchingSchemas, options); + } + _validateNode(); + + matchingSchemas.add({ node: node, schema: schema }); + + function _validateNode(): void { + if (schema === undefined) { + return; + } + + function matchesType(type: string): boolean { + return node.type === type || (type === "integer" && node.type === "number" && node.isInteger); + } + + if (Array.isArray(schema.type)) { + if (!schema.type.some(matchesType)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || `Incorrect type. Expected one of ${(schema.type).join(", ")}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + problemType: ProblemType.typeMismatchWarning, + problemArgs: [(schema.type).join(", ")], + }); + } + } else if (schema.type) { + if (!matchesType(schema.type)) { + //get more specific name than just object + const schemaType = schema.type === "object" ? schema.title ?? "" : schema.type; + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || getWarningMessage(ProblemType.typeMismatchWarning, [schemaType]), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + problemType: ProblemType.typeMismatchWarning, + problemArgs: [schemaType], + }); + } + } + if (Array.isArray(schema.allOf)) { + for (const subSchemaRef of schema.allOf) { + validate(node, asSchema(subSchemaRef), schema, validationResult, matchingSchemas, options); + } + } + } + + const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean): number => { + const matches = []; + const subMatches = []; + const noPropertyMatches = []; + // remember the best match that is used for error messages + let bestMatch: { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + } | null = null; + for (const subSchemaRef of alternatives) { + const subSchema = { ...asSchema(subSchemaRef) }; + const subValidationResult = new ValidationResult(); + const subMatchingSchemas = matchingSchemas.newSub(); + validate(node, subSchema, schema!, subValidationResult, subMatchingSchemas, options); + if (!subValidationResult.hasProblems() || callFromAutoComplete) { + matches.push(subSchema); + subMatches.push(subSchema); + if (subValidationResult.propertiesMatches === 0) { + noPropertyMatches.push(subSchema); + } + if (subSchema.format) { + subMatches.pop(); + } + } + if (!bestMatch) { + bestMatch = { + schema: subSchema, + validationResult: subValidationResult, + matchingSchemas: subMatchingSchemas, + }; + } else { + bestMatch = genericComparison(node, maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas); + } + } + + if (subMatches.length > 1 && (subMatches.length > 1 || noPropertyMatches.length === 0) && maxOneMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: 1 }, + severity: DiagnosticSeverity.Warning, + message: "Matches multiple schemas when only one must validate.", + source: getSchemaSource(schema!), + schemaUri: getSchemaUri(schema!, originalSchema), + problemArgs: [subMatches.map((s) => getSchemaSource(s)).join(", ")], + problemType: ProblemType.typeMismatchWarning, + }); + } + if (bestMatch !== null) { + validationResult.merge(bestMatch.validationResult); + validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; + validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; + validationResult.enumValueMatch = validationResult.enumValueMatch || bestMatch.validationResult.enumValueMatch; + if (bestMatch.validationResult.enumValues?.length) { + validationResult.enumValues = (validationResult.enumValues || []).concat(bestMatch.validationResult.enumValues); + } + matchingSchemas.merge(bestMatch.matchingSchemas); + } + return matches.length; + }; + if (Array.isArray(schema.anyOf)) { + testAlternatives(schema.anyOf, false); + } + if (Array.isArray(schema.oneOf)) { + testAlternatives(schema.oneOf, true); + } + + if (Array.isArray(schema.enum)) { + const val = getNodeValue(node); + let enumValueMatch = false; + for (const e of schema.enum) { + if (equals(val, e) || (callFromAutoComplete && isString(val) && isString(e) && val && e.startsWith(val))) { + enumValueMatch = true; + break; + } + } + validationResult.enumValues = schema.enum; + validationResult.enumValueMatch = enumValueMatch; + if (!enumValueMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + message: + schema.errorMessage || + `Value is not accepted. Valid values: ${schema.enum + .map((v) => { + return JSON.stringify(v); + }) + .join(", ")}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isDefined(schema.const)) { + const val = getNodeValue(node); + if ( + !equals(val, schema.const) && + !(callFromAutoComplete && isString(val) && isString(schema.const) && schema.const.startsWith(val)) + ) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + problemType: ProblemType.constWarning, + message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(schema.const)]), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [JSON.stringify(schema.const)], + }); + validationResult.enumValueMatch = false; + } else { + validationResult.enumValueMatch = true; + } + validationResult.enumValues = [schema.const]; + } + + if (schema.deprecationMessage && node.parent) { + validationResult.problems.push({ + location: { offset: node.parent.offset, length: node.parent.length }, + severity: DiagnosticSeverity.Warning, + message: schema.deprecationMessage, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + function _validateNumberNode(node: NumberASTNode, schema: JSONSchema, validationResult: ValidationResult): void { + const val = node.value; + + if (isNumber(schema.multipleOf)) { + if (val % schema.multipleOf !== 0) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is not divisible by ${schema.multipleOf}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + function getExclusiveLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (isNumber(exclusive)) { + return exclusive; + } + if (isBoolean(exclusive) && exclusive) { + return limit; + } + return undefined; + } + function getLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (!isBoolean(exclusive) || !exclusive) { + return limit; + } + return undefined; + } + const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the exclusive minimum of ${exclusiveMinimum}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is above the exclusive maximum of ${exclusiveMaximum}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const minimum = getLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(minimum) && val < minimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the minimum of ${minimum}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const maximum = getLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(maximum) && val > maximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the maximum of ${maximum}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + function _validateStringNode(node: StringASTNode, schema: JSONSchema, validationResult: ValidationResult): void { + if (isNumber(schema.minLength) && node.value.length < schema.minLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `String is shorter than the minimum length of ${schema.minLength}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isNumber(schema.maxLength) && node.value.length > schema.maxLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `String is longer than the maximum length of ${schema.maxLength}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isString(schema.pattern)) { + const regex = safeCreateUnicodeRegExp(schema.pattern); + if (!regex.test(node.value)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: + schema.patternErrorMessage || + schema.errorMessage || + `String does not match the pattern of "${schema.pattern}".`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (schema.format) { + switch (schema.format) { + case "uri": + case "uri-reference": + { + let errorMessage; + if (!node.value) { + errorMessage = "URI expected."; + } else { + try { + const uri = URI.parse(node.value); + if (!uri.scheme && schema.format === "uri") { + errorMessage = "URI with a scheme is expected."; + } + } catch (e) { + errorMessage = (e as Error).message; + } + } + if (errorMessage) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || `String is not a URI: ${errorMessage}`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + break; + case "color-hex": + case "date-time": + case "date": + case "time": + case "email": + case "ipv4": + case "ipv6": + { + const format = formats[schema.format]; + if (!node.value || !format.pattern.test(node.value)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + break; + default: + } + } + } + function _validateArrayNode( + node: ArrayASTNode, + schema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector + ): void { + if (Array.isArray(schema.items)) { + const subSchemas = schema.items; + for (let index = 0; index < subSchemas.length; index++) { + const subSchemaRef = subSchemas[index]; + const subSchema = asSchema(subSchemaRef); + const itemValidationResult = new ValidationResult(); + const item = node.items[index]; + if (item) { + validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } else if (node.items.length >= subSchemas.length) { + validationResult.propertiesValueMatches++; + } + } + if (node.items.length > subSchemas.length) { + if (typeof schema.additionalItems === "object") { + for (let i = subSchemas.length; i < node.items.length; i++) { + const itemValidationResult = new ValidationResult(); + validate( + node.items[i], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.additionalItems, + schema, + itemValidationResult, + matchingSchemas, + options + ); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } + } else if (schema.additionalItems === false) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too many items according to schema. Expected ${subSchemas.length} or fewer.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } else { + const itemSchema = asSchema(schema.items); + if (itemSchema) { + const itemValidationResult = new ValidationResult(); + node.items.forEach((item) => { + if (itemSchema.oneOf && itemSchema.oneOf.length === 1) { + const subSchemaRef = itemSchema.oneOf[0]; + const subSchema = { ...asSchema(subSchemaRef) }; + subSchema.title = schema.title; + // subSchema.closestTitle = schema.closestTitle; + validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } else { + validate(item, itemSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } + }); + } + } + + const containsSchema = asSchema(schema.contains); + if (containsSchema) { + const doesContain = node.items.some((item) => { + const itemValidationResult = new ValidationResult(); + validate(item, containsSchema, schema, itemValidationResult, NoOpSchemaCollector.instance, options); + return !itemValidationResult.hasProblems(); + }); + + if (!doesContain) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || "Array does not contain required item.", + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isNumber(schema.minItems) && node.items.length < schema.minItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too few items. Expected ${schema.minItems} or more.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isNumber(schema.maxItems) && node.items.length > schema.maxItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too many items. Expected ${schema.maxItems} or fewer.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (schema.uniqueItems === true) { + const values: unknown[] = getNodeValue(node); + const duplicates = values.some((value, index) => { + return index !== values.lastIndexOf(value); + }); + if (duplicates) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: "Array has duplicate items.", + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } + + function _validateObjectNode( + node: ObjectASTNode, + schema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector + ): void { + const seenKeys: { [key: string]: ASTNode } = Object.create(null); + const unprocessedProperties: string[] = []; + const unprocessedNodes: PropertyASTNode[] = [...node.properties]; + + while (unprocessedNodes.length > 0) { + const propertyNode = unprocessedNodes.pop(); + if (!propertyNode) { + continue; + } + const key = propertyNode.keyNode.value; + + //Replace the merge key with the actual values of what the node value points to in seen keys + if (key === "<<" && propertyNode.valueNode) { + switch (propertyNode.valueNode.type) { + case "object": { + unprocessedNodes.push(...propertyNode.valueNode["properties"]); + break; + } + case "array": { + propertyNode.valueNode["items"].forEach((sequenceNode) => { + if (sequenceNode && isIterable((sequenceNode as ObjectASTNode)["properties"])) { + unprocessedNodes.push(...(sequenceNode as ObjectASTNode)["properties"]); + } + }); + break; + } + default: { + break; + } + } + } else { + seenKeys[key] = propertyNode.valueNode as ASTNode; + unprocessedProperties.push(key); + } + } + + if (Array.isArray(schema.required)) { + for (const propertyName of schema.required) { + if (seenKeys[propertyName] === undefined) { + const keyNode = node.parent && node.parent.type === "property" && node.parent.keyNode; + const location = keyNode + ? { offset: keyNode.offset, length: keyNode.length } + : { offset: node.offset, length: 1 }; + validationResult.problems.push({ + location: location, + severity: DiagnosticSeverity.Warning, + message: getWarningMessage(ProblemType.missingRequiredPropWarning, [propertyName]), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [propertyName], + problemType: ProblemType.missingRequiredPropWarning, + }); + } + } + } + + const propertyProcessed = (prop: string): void => { + let index = unprocessedProperties.indexOf(prop); + while (index >= 0) { + unprocessedProperties.splice(index, 1); + index = unprocessedProperties.indexOf(prop); + } + }; + + if (schema.properties) { + for (const propertyName of Object.keys(schema.properties)) { + propertyProcessed(propertyName); + const propertySchema = schema.properties[propertyName]; + const child = seenKeys[propertyName]; + if (child) { + if (isBoolean(propertySchema)) { + if (!propertySchema) { + const propertyNode = child.parent; + validationResult.problems.push({ + location: { + offset: propertyNode.keyNode.offset, + length: propertyNode.keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + propertySchema.url = schema.url ?? originalSchema.url; + const propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } + } + + if (schema.patternProperties) { + for (const propertyPattern of Object.keys(schema.patternProperties)) { + const regex = safeCreateUnicodeRegExp(propertyPattern); + for (const propertyName of unprocessedProperties.slice(0)) { + if (regex.test(propertyName)) { + propertyProcessed(propertyName); + const child = seenKeys[propertyName]; + if (child) { + const propertySchema = schema.patternProperties[propertyPattern]; + if (isBoolean(propertySchema)) { + if (!propertySchema) { + const propertyNode = child.parent; + validationResult.problems.push({ + location: { + offset: propertyNode.keyNode.offset, + length: propertyNode.keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + const propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } + } + } + } + if (typeof schema.additionalProperties === "object") { + for (const propertyName of unprocessedProperties) { + const child = seenKeys[propertyName]; + if (child) { + const propertyValidationResult = new ValidationResult(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate(child, schema.additionalProperties, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } else if ( + schema.additionalProperties === false || + (schema.type === "object" && + schema.additionalProperties === undefined && + options.disableAdditionalProperties === true) + ) { + if (unprocessedProperties.length > 0) { + const possibleProperties = + schema.properties && Object.keys(schema.properties).filter((prop) => !seenKeys[prop]); + + for (const propertyName of unprocessedProperties) { + const child = seenKeys[propertyName]; + if (child) { + let propertyNode = null; + if (child.type !== "property") { + propertyNode = child.parent as ASTNode; + if (propertyNode.type === "object") { + propertyNode = propertyNode.properties[0]; + } + } else { + propertyNode = child; + } + const problem: IProblem = { + location: { + offset: (propertyNode as PropertyASTNode).keyNode.offset, + length: (propertyNode as PropertyASTNode).keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }; + if (possibleProperties?.length) { + problem.data = { properties: possibleProperties }; + } + validationResult.problems.push(problem); + } + } + } + } + + if (isNumber(schema.maxProperties)) { + if (node.properties.length > schema.maxProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Object has more properties than limit of ${schema.maxProperties}.`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isNumber(schema.minProperties)) { + if (node.properties.length < schema.minProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Object has fewer properties than the required number of ${schema.minProperties}`, + source: getSchemaSource(schema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } + + //genericComparison tries to find the best matching schema using a generic comparison + function genericComparison( + node: ASTNode, + maxOneMatch: boolean, + subValidationResult: ValidationResult, + bestMatch: { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + }, + subSchema: JSONSchema, + subMatchingSchemas: ISchemaCollector + ): { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + } { + if ( + !maxOneMatch && + !subValidationResult.hasProblems() && + (!bestMatch.validationResult.hasProblems() || callFromAutoComplete) + ) { + // no errors, both are equally good matches + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; + bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + } else { + const compareResult = subValidationResult.compareGeneric(bestMatch.validationResult); + if ( + compareResult > 0 || + (compareResult === 0 && + maxOneMatch && + bestMatch.schema.type === "object" && + node.type !== "null" && + node.type !== bestMatch.schema.type) + ) { + // our node is the best matching so far + bestMatch = { + schema: subSchema, + validationResult: subValidationResult, + matchingSchemas: subMatchingSchemas, + }; + } else if (compareResult === 0) { + // there's already a best matching but we are as good + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.mergeEnumValues(subValidationResult); + bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [ + ProblemType.missingRequiredPropWarning, + ProblemType.typeMismatchWarning, + ProblemType.constWarning, + ]); + } + } + return bestMatch; + } +} + +export function equals(one: unknown, other: unknown): boolean { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== "object") { + return false; + } + if (Array.isArray(one) !== Array.isArray(other)) { + return false; + } + + let i: number, key: string; + + if (Array.isArray(one) && Array.isArray(other)) { + if (one.length !== other.length) { + return false; + } + for (i = 0; i < one.length; i++) { + if (!equals(one[i], other[i])) { + return false; + } + } + } else { + const oneKeys: string[] = []; + + for (key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + const otherKeys: string[] = []; + for (const key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!equals(oneKeys, otherKeys)) { + return false; + } + for (let i = 0; i < oneKeys.length; i++) { + if (!equals(one[oneKeys[i] as keyof typeof one], other[oneKeys[i] as keyof typeof other])) { + return false; + } + } + } + return true; +} + +export function isNumber(val: unknown): val is number { + return typeof val === "number"; +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export function isDefined(val: unknown): val is object | string | number | boolean { + return typeof val !== "undefined"; +} + +export function isBoolean(val: unknown): val is boolean { + return typeof val === "boolean"; +} + +export function isString(val: unknown): val is string { + return typeof val === "string"; +} + +/** + * Check that provided value is Iterable + * @param val the value to check + * @returns true if val is iterable, false otherwise + */ +export function isIterable(val: unknown): boolean { + return Symbol.iterator in Object(val); +} + +export function safeCreateUnicodeRegExp(pattern: string): RegExp { + // fall back to regular regexp if we cannot create Unicode one + try { + return new RegExp(pattern, "u"); + } catch (ignore) { + return new RegExp(pattern); + } +} + +export function convertSimple2RegExpPattern(pattern: string): string { + return pattern.replace(/[-\\{}+?|^$.,[\]()#\s]/g, "\\$&").replace(/[*]/g, ".*"); +} + +/** + * check all the schemas which is inside anyOf presented or not in matching schema. + * @param node node + * @param matchingSchemas all matching schema + * @param schema scheam which is having anyOf + * @returns true if all the schemas which inside anyOf presents in matching schema + */ +export function isAllSchemasMatched(node: ASTNode, matchingSchemas: IApplicableSchema[], schema: JSONSchema): boolean { + let count = 0; + for (const matchSchema of matchingSchemas) { + if (node === matchSchema.node && matchSchema.schema !== schema) { + schema.anyOf?.forEach((childSchema: JSONSchema) => { + if ( + matchSchema.schema.title === childSchema.title && + matchSchema.schema.description === childSchema.description && + matchSchema.schema.properties === childSchema.properties + ) { + count++; + } + }); + } + } + return count === schema.anyOf?.length; +} diff --git a/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts b/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts new file mode 100644 index 0000000..ca08873 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface JSONSchema { + id?: string; + $schema?: string; + type?: string | string[]; + title?: string; + default?: any; + definitions?: JSONSchemaMap; + description?: string; + properties?: JSONSchemaMap; + patternProperties?: JSONSchemaMap; + additionalProperties?: any; + minProperties?: number; + maxProperties?: number; + dependencies?: JSONSchemaMap | string[]; + items?: any; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalItems?: boolean; + pattern?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; + multipleOf?: number; + required?: string[]; + firstProperty?: string[]; + $ref?: string; + anyOf?: JSONSchema[]; + allOf?: JSONSchema[]; + oneOf?: JSONSchema[]; + not?: JSONSchema; + enum?: any[]; + format?: string; + errorMessage?: string; + patternErrorMessage?: string; + deprecationMessage?: string; + doNotSuggest?: boolean; + enumDescriptions?: string[]; + ignoreCase?: string; + aliases?: string[]; + document?: { [key: string]: string }; + $id?: string; + insertText?: string; + triggerSuggest?: boolean; + // Extra + url?: string; + const?: any; + contains?: JSONSchema; +} + +export interface JSONSchemaMap { + [name: string]: JSONSchema; +} + +export type JSONSchemaRef = JSONSchema | boolean; + +export class UnresolvedSchema { + schema: JSONSchema; + errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } +} + +export class ResolvedSchema { + schema: JSONSchema; + errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } +} diff --git a/server/packages/workflow-tests-language-service/src/schema/provider.ts b/server/packages/workflow-tests-language-service/src/schema/provider.ts new file mode 100644 index 0000000..7309480 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/provider.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable } from "inversify"; +import { JSONSchema, ResolvedSchema, UnresolvedSchema } from "./jsonSchema"; +import WorkflowTestsSchema from "../../../../../workflow-languages/schemas/tests.schema.json"; + +export interface WorkflowTestsSchemaProvider { + getResolvedSchema(): ResolvedSchema; +} + +@injectable() +export class WorkflowTestsSchemaProviderImpl implements WorkflowTestsSchemaProvider { + private readonly resolvedSchema: ResolvedSchema; + + constructor() { + this.resolvedSchema = this.resolveSchemaContent(new UnresolvedSchema(WorkflowTestsSchema, [])); + } + + public getResolvedSchema(): ResolvedSchema { + return this.resolvedSchema; + } + + private resolveSchemaContent(schemaToResolve: UnresolvedSchema): ResolvedSchema { + const resolveErrors: string[] = schemaToResolve.errors.slice(0); + const schema = schemaToResolve.schema; + + const findSection = (schema: JSONSchema, path: string): any => { + if (!path) { + return schema; + } + let current: any = schema; + if (path[0] === "/") { + path = path.substring(1); + } + path.split("/").some((part) => { + current = current[part]; + return !current; + }); + return current; + }; + + const resolveLink = (node: any, linkedSchema: JSONSchema, linkPath: string): void => { + const section = findSection(linkedSchema, linkPath); + if (section) { + for (const key in section) { + if (Object.prototype.hasOwnProperty.call(section, key) && !Object.prototype.hasOwnProperty.call(node, key)) { + node[key] = section[key]; + } + } + } else { + resolveErrors.push(`json.schema.invalidref: $ref '${linkPath}' in ${linkedSchema.id} can not be resolved.`); + } + delete node.$ref; + }; + + const resolveRefs = (node: JSONSchema, parentSchema: JSONSchema): void => { + if (!node) { + return; + } + + const toWalk: JSONSchema[] = [node]; + const seen: JSONSchema[] = []; + + const collectEntries = (...entries: JSONSchema[]): void => { + for (const entry of entries) { + if (typeof entry === "object") { + toWalk.push(entry); + } + } + }; + + const collectMapEntries = (...maps: JSONSchema[]): void => { + for (const map of maps) { + if (typeof map === "object") { + for (const key in map) { + if (Object.prototype.hasOwnProperty.call(map, key)) { + const entry = (map as any)[key]; + toWalk.push(entry); + } + } + } + } + }; + + const collectArrayEntries = (...arrays: JSONSchema[][]): void => { + for (const array of arrays) { + if (Array.isArray(array)) { + toWalk.push(...array); + } + } + }; + + while (toWalk.length) { + const next = toWalk.pop(); + if (!next) { + break; + } + if (seen.indexOf(next) >= 0) { + continue; + } + seen.push(next); + if (next.$ref) { + const segments = next.$ref.split("#", 2); + resolveLink(next, parentSchema, segments[1]); + } + collectEntries(next.items, next.additionalProperties, next.not as any); + collectMapEntries( + next.definitions as JSONSchema, + next.properties as JSONSchema, + next.patternProperties as JSONSchema, + next.dependencies as JSONSchema + ); + collectArrayEntries( + next.anyOf as JSONSchema[], + next.allOf as JSONSchema[], + next.oneOf as JSONSchema[], + next.items as JSONSchema[] + ); + } + }; + resolveRefs(schema, schema); + return new ResolvedSchema(schema, resolveErrors); + } +} diff --git a/server/packages/workflow-tests-language-service/src/schema/service.ts b/server/packages/workflow-tests-language-service/src/schema/service.ts new file mode 100644 index 0000000..709404f --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/service.ts @@ -0,0 +1,49 @@ +import { DocumentContext } from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-types"; +import { TYPES } from "../types"; +import { IApplicableSchema, JSONSchemaService } from "./adapter"; +import { ResolvedSchema } from "./jsonSchema"; +import { WorkflowTestsSchemaProvider } from "./provider"; + +export interface WorkflowTestsSchemaService { + schema: ResolvedSchema; + validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined; + getMatchingSchemas( + documentContext: DocumentContext, + nodeOffset?: number | undefined, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[]; +} + +@injectable() +export class WorkflowTestsSchemaServiceImpl implements WorkflowTestsSchemaService { + constructor( + @inject(TYPES.WorkflowTestsSchemaProvider) protected schemaProvider: WorkflowTestsSchemaProvider, + @inject(TYPES.JSONSchemaService) protected jsonSchemaService: JSONSchemaService + ) {} + + get schema(): ResolvedSchema { + return this.schemaProvider.getResolvedSchema(); + } + + validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined { + const resolvedSchema = this.schemaProvider.getResolvedSchema(); + return this.jsonSchemaService.validate(documentContext, resolvedSchema.schema, severity); + } + + getMatchingSchemas( + documentContext: DocumentContext, + nodeOffset?: number | undefined, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[] { + const resolvedSchema = this.schemaProvider.getResolvedSchema(); + return this.jsonSchemaService.getMatchingSchemas( + documentContext, + resolvedSchema.schema, + nodeOffset, + null, + didCallFromAutoComplete + ); + } +} diff --git a/server/packages/workflow-tests-language-service/src/services/completion/completion.ts b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts new file mode 100644 index 0000000..fa99764 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts @@ -0,0 +1,27 @@ +import { CompletionList, DocumentContext, Position } from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { WorkflowTestsSchemaService } from "../../schema/service"; +import { TYPES } from "../../types"; +import { YAMLCompletionHelper } from "./helper"; + +export interface WorkflowTestsCompletionService { + doComplete(documentContext: DocumentContext, position: Position): Promise; +} + +/** + * Simple wrapper around the YAMLCompletionHelper to combine it with custom completion logic. + */ +@injectable() +export class WorkflowTestsCompletionServiceImpl implements WorkflowTestsCompletionService { + private yamlCompletionHelper: YAMLCompletionHelper; + + constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) { + this.yamlCompletionHelper = new YAMLCompletionHelper(schemaService); + } + + public async doComplete(documentContext: DocumentContext, position: Position): Promise { + // TODO: Add custom completion logic specific to workflow test files here + const result = await this.yamlCompletionHelper.doComplete(documentContext, position); + return result; + } +} diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts new file mode 100644 index 0000000..c0ec3b6 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -0,0 +1,1732 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * This is a modified version of the original yamlCompletion logic from the yaml-language-server. + * The original file can be found here: https://github.com/redhat-developer/yaml-language-server/blob/main/src/languageservice/services/yamlCompletion.ts#L1601 + * + * The reason for this is that the original dependency is not compatible with the browser version of the language server. + * In addition, there are some differences in the way we handle the AST and the schemas. + */ + +import { + CompletionItem as CompletionItemBase, + CompletionItemKind, + CompletionList, + DocumentContext, + InsertTextFormat, + MarkupContent, + MarkupKind, + Position, + Range, + TextEdit, + WorkflowInput, + WorkflowOutput, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; +import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocument"; +import { HasRange, indexOf, isMapContainsEmptyPair, rangeMatches } from "@gxwf/yaml-language-service/src/utils"; +import { guessIndentation } from "@gxwf/yaml-language-service/src/utils/indentationGuesser"; +import { TextBuffer } from "@gxwf/yaml-language-service/src/utils/textBuffer"; +import { Node, Pair, YAMLMap, YAMLSeq, Range as YamlRange, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; +import { isDefined, isString } from "../../schema/adapter"; +import { JSONSchema, JSONSchemaRef } from "../../schema/jsonSchema"; +import { WorkflowTestsSchemaService } from "../../schema/service"; + +const doubleQuotesEscapeRegExp = /[\\]+"/g; +const parentCompletionKind = CompletionItemKind.Class; +const existingProposeItem = "__"; + +interface ParentCompletionItemOptions { + schema: JSONSchema; + indent?: string; + insertTexts?: string[]; +} + +interface CompletionItem extends CompletionItemBase { + parent?: ParentCompletionItemOptions; +} + +interface CompletionsCollector { + add(suggestion: CompletionItem, oneOfSchema?: boolean): void; + error(message: string): void; + log(message: string): void; + getNumberOfProposals(): number; + result: CompletionList; + proposed: { [key: string]: CompletionItem }; +} + +interface InsertText { + insertText: string; + insertIndex: number; +} + +export class YAMLCompletionHelper { + private indentation: string = " "; + private arrayPrefixIndentation: string = ""; + private workflowInputs: WorkflowInput[] = []; + private workflowOutputs: WorkflowOutput[] = []; + + constructor(protected schemaService: WorkflowTestsSchemaService) {} + + private get newTestSnippet(): string { + return `- doc: \${1:TODO write test description} +${this.indentation}job: +${this.indentation}${this.indentation}$0 +`; + } + + private get newTestSnippetCompletion(): CompletionItem { + const completionItem: CompletionItem = { + label: "- doc:", + labelDetails: { detail: "New Workflow Test" }, + documentation: { + kind: MarkupKind.Markdown, + value: + "Create a new workflow test definition.\n\nYou can provide a `description` for the test and then press `Tab` to continue defining input jobs.", + }, + kind: CompletionItemKind.Property, + insertText: this.newTestSnippet, + insertTextFormat: InsertTextFormat.Snippet, + }; + return completionItem; + } + + public async doComplete(documentContext: DocumentContext, position: Position): Promise { + const result = CompletionList.create([], false); + + const document = documentContext.textDocument; + const textBuffer = new TextBuffer(document); + + const indent = guessIndentation(textBuffer, 2, true); + this.indentation = indent.insertSpaces ? " ".repeat(indent.tabSize) : "\t"; + + const offset = document.offsetAt(position); + const text = document.getText(); + + if (text.charAt(offset - 1) === ":") { + return Promise.resolve(result); + } + + let currentDoc = documentContext.internalDocument as YAMLSubDocument; + if (currentDoc === null) { + return Promise.resolve(result); + } + // as we modify AST for completion, we need to use copy of original document + currentDoc = currentDoc.clone(); + let [node, foundByClosest] = currentDoc.getNodeFromPosition(offset, textBuffer, this.indentation.length); + + if (!node) { + result.items.push(this.newTestSnippetCompletion); + return Promise.resolve(result); + } + + const currentWord = textBuffer.getCurrentWord(offset); + let lineContent = textBuffer.getLineContent(position.line); + const lineAfterPosition = lineContent.substring(position.character); + const areOnlySpacesAfterPosition = /^[ ]+\n?$/.test(lineAfterPosition); + + // if the line is empty, or only contains a dash without indentation, + // we suggest a new test snippet + if (lineContent.match(/^(\n|-|- {1}|-\n)$/)) { + result.items.push(this.newTestSnippetCompletion); + return Promise.resolve(result); + } + + // Gather all the workflow information needed to provide completions + const testDocument = documentContext as WorkflowTestsDocument; + this.workflowInputs = await testDocument.getWorkflowInputs(); + this.workflowOutputs = await testDocument.getWorkflowOutputs(); + + let overwriteRange: Range | null = null; + if (areOnlySpacesAfterPosition) { + overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); + const isOnlyWhitespace = lineContent.trim().length === 0; + const isOnlyDash = lineContent.match(/^\s*(-)\s*$/); + if (node && isScalar(node) && !isOnlyWhitespace && !isOnlyDash) { + const lineToPosition = lineContent.substring(0, position.character); + const matches = + // get indentation of unfinished property (between indent and cursor) + lineToPosition.match(/^[\s-]*([^:]+)?$/) || + // OR get unfinished value (between colon and cursor) + lineToPosition.match(/:[ \t]((?!:[ \t]).*)$/); + + if (matches?.[1]) { + overwriteRange = Range.create( + Position.create(position.line, position.character - matches[1].length), + Position.create(position.line, lineContent.length) + ); + } + } + } else if (node && isScalar(node) && node.value === "null") { + const nodeStartPos = document.positionAt(node.range?.[0] ?? 0); + nodeStartPos.character += 1; + const nodeEndPos = document.positionAt(node.range?.[2] ?? 0); + nodeEndPos.character += 1; + overwriteRange = Range.create(nodeStartPos, nodeEndPos); + } else if (node && isScalar(node) && node.value) { + const start = document.positionAt(node.range?.[0] ?? 0); + overwriteRange = Range.create(start, document.positionAt(node.range?.[1] ?? 0)); + } else if (node && isScalar(node) && node.value === null && currentWord === "-") { + overwriteRange = Range.create(position, position); + this.arrayPrefixIndentation = " "; + } else { + let overwriteStart = offset - currentWord.length; + if (overwriteStart > 0 && text[overwriteStart - 1] === '"') { + overwriteStart--; + } + overwriteRange = Range.create(document.positionAt(overwriteStart), position); + } + + const proposed: { [key: string]: CompletionItem } = {}; + const collector: CompletionsCollector = { + add: (completionItem: CompletionItem, oneOfSchema: boolean) => { + const addSuggestionForParent = function (completionItem: CompletionItem): void { + const existsInYaml = proposed[completionItem.label]?.label === existingProposeItem; + //don't put to parent suggestion if already in yaml + if (existsInYaml) { + return; + } + const schema = completionItem.parent?.schema; + const schemaType = schema?.title ?? "unknown schema type"; + const schemaDescription = schema?.description; + + let parentCompletion: CompletionItem | undefined = result.items.find( + (item: CompletionItem) => item.parent?.schema === schema && item.kind === parentCompletionKind + ); + + if (!completionItem.insertText) { + completionItem.insertText = completionItem.label; + } + + if (!parentCompletion) { + // create a new parent + parentCompletion = { + ...completionItem, + documentation: schemaDescription, + sortText: "_" + schemaType, // this parent completion goes first, + kind: parentCompletionKind, + }; + parentCompletion.label = parentCompletion.label || completionItem.label; + result.items.push(parentCompletion); + } else if (parentCompletion.parent?.insertTexts?.includes(completionItem.insertText)) { + // already exists in the parent + return; + } else { + // add to the existing parent + parentCompletion.parent?.insertTexts?.push(completionItem.insertText); + } + }; + + const isForParentCompletion = !!completionItem.parent; + let label = completionItem.label; + if (!label) { + // we receive not valid CompletionItem as `label` is mandatory field, so just ignore it + console.warn(`Ignoring CompletionItem without label: ${JSON.stringify(completionItem)}`); + return; + } + if (!isString(label)) { + label = String(label); + } + + label = label.replace(/[\n]/g, "↵"); + if (label.length > 60) { + const shortendedLabel = label.substr(0, 57).trim() + "..."; + if (!proposed[shortendedLabel]) { + label = shortendedLabel; + } + } + + // trim $1 from end of completion + if (completionItem.insertText) { + if (completionItem.insertText.endsWith("$1") && !isForParentCompletion) { + completionItem.insertText = completionItem.insertText.substr(0, completionItem.insertText.length - 2); + } + if (overwriteRange && overwriteRange.start.line === overwriteRange.end.line) { + completionItem.textEdit = TextEdit.replace(overwriteRange, completionItem.insertText); + } + } + + completionItem.label = label; + + if (isForParentCompletion) { + addSuggestionForParent(completionItem); + return; + } + + if (this.arrayPrefixIndentation) { + this.updateCompletionText(completionItem, this.arrayPrefixIndentation + completionItem.insertText); + } + + const existing = proposed[label]; + const isInsertTextDifferent = + existing?.label !== existingProposeItem && existing?.insertText !== completionItem.insertText; + if (!existing) { + proposed[label] = completionItem; + result.items.push(completionItem); + } else if (existing.insertText && completionItem.insertText && isInsertTextDifferent) { + // try to merge simple insert values + const mergedText = this.mergeSimpleInsertTexts( + label, + existing.insertText, + completionItem.insertText, + oneOfSchema + ); + if (mergedText) { + this.updateCompletionText(existing, mergedText); + } else { + // add to result when it wasn't able to merge (even if the item is already there but with a different value) + proposed[label] = completionItem; + result.items.push(completionItem); + } + } + if (existing && !existing.documentation && completionItem.documentation) { + existing.documentation = completionItem.documentation; + } + }, + error: (message: string) => { + console.error(message); + }, + log: (message: string) => { + console.log(message); + }, + getNumberOfProposals: () => { + return result.items.length; + }, + result, + proposed, + }; + + if (lineContent.endsWith("\n")) { + lineContent = lineContent.substring(0, lineContent.length - 1); + } + + try { + const schema = this.schemaService.schema; + + if (!schema || schema.errors.length) { + return result; + } + + let currentProperty: YamlNode | null = null; + + if (!node) { + if (!currentDoc.internalDocument.contents || isScalar(currentDoc.internalDocument.contents)) { + const map = currentDoc.internalDocument.createNode({}); + map.range = [offset, offset + 1, offset + 1]; + currentDoc.internalDocument.contents = map; + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = currentDoc.findClosestNode(offset, textBuffer); + foundByClosest = true; + } + } + + const originalNode = node!; + if (node) { + if (lineContent.length === 0) { + node = currentDoc.internalDocument.contents as Node; + } else { + const parent = currentDoc.getParent(node); + if (parent) { + if (isScalar(node)) { + if (node.value) { + if (isPair(parent)) { + if (parent.value === node) { + if (lineContent.trim().length > 0 && lineContent.indexOf(":") < 0) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + const parentParent = currentDoc.getParent(parent); + if (isSeq(currentDoc.internalDocument.contents)) { + const index = indexOf(currentDoc.internalDocument.contents, parent); + if (typeof index === "number") { + currentDoc.internalDocument.set(index, map); + currentDoc.updateFromInternalDocument(); + } + } else if (parentParent && (isMap(parentParent) || isSeq(parentParent))) { + parentParent.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } else { + currentDoc.internalDocument.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } + + currentProperty = (map as YAMLMap).items[0]; + node = map; + } else if (lineContent.trim().length === 0) { + const parentParent = currentDoc.getParent(parent); + if (parentParent) { + node = parentParent; + } + } + } else if (parent.key === node) { + const parentParent = currentDoc.getParent(parent); + currentProperty = parent; + if (parentParent) { + node = parentParent; + } + } + } else if (isSeq(parent)) { + if (lineContent.trim().length > 0) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = parent; + } + } + } else if (node.value === null) { + if (isPair(parent)) { + if (parent.key === node) { + node = parent; + } else { + if (isNode(parent.key) && parent.key.range) { + const parentParent = currentDoc.getParent(parent); + if ( + foundByClosest && + parentParent && + isMap(parentParent) && + isMapContainsEmptyPair(parentParent) + ) { + node = parentParent; + } else { + const parentPosition = document.positionAt(parent.key.range[0]); + //if cursor has bigger indentation that parent key, then we need to complete new empty object + if (position.character > parentPosition.character && position.line !== parentPosition.line) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + + if (parentParent && (isMap(parentParent) || isSeq(parentParent))) { + parentParent.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } else { + currentDoc.internalDocument.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } + currentProperty = (map as YAMLMap).items[0]; + node = map; + } else if (parentPosition.character === position.character) { + if (parentParent) { + node = parentParent; + } + } + } + } + } + } else if (isSeq(parent)) { + if (lineContent.charAt(position.character - 1) !== "-") { + const map = this.createTempObjNode(currentWord, node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else if (lineContent.charAt(position.character - 1) === "-") { + const map = this.createTempObjNode("", node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = parent; + } + } + } + } else if (isMap(node)) { + if (!foundByClosest && lineContent.trim().length === 0 && isSeq(parent)) { + const nextLine = textBuffer.getLineContent(position.line + 1); + if (textBuffer.getLineCount() === position.line + 1 || nextLine.trim().length === 0) { + node = parent; + } + } + } + } else if (isScalar(node)) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + currentDoc.internalDocument.contents = map; + currentDoc.updateFromInternalDocument(); + currentProperty = map.items[0]; + node = map; + } else if (isMap(node)) { + for (const pair of node.items) { + if (isNode(pair.value) && pair.value.range && pair.value.range[0] === offset + 1) { + node = pair.value; + } + } + } else if (isSeq(node)) { + if (lineContent.charAt(position.character - 1) !== "-") { + /** + * It the indentation of the current line matches the indentation of the item in the sequence node + * then we are at the same level as the item in the sequence so it should be a sibling of the item + */ + let range: YamlRange | undefined = undefined; + const lastItem = node.items[node.items.length - 1] as YAMLMap; + if (lastItem) { + node = lastItem; + range = lastItem.range ?? undefined; + } + + const map = this.createTempObjNode(currentWord, node, currentDoc, range); + map.items = []; + currentDoc.updateFromInternalDocument(); + for (const pair of node.items) { + map.items.push(pair as Pair); + } + node = map; + } + } + } + } + + // completion for object keys + if (node && isMap(node)) { + // don't suggest properties that are already present + const properties = node.items; + for (const p of properties) { + if (!currentProperty || currentProperty !== p) { + if (isScalar(p.key)) { + proposed[p.key.value + ""] = CompletionItemBase.create(existingProposeItem); + } + } + } + + await this.addPropertyCompletions( + documentContext, + currentDoc, + node, + originalNode, + "", + collector, + textBuffer, + overwriteRange + ); + + if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { + collector.add({ + kind: CompletionItemKind.Property, + label: currentWord, + insertText: this.getInsertTextForProperty(currentWord, null, ""), + insertTextFormat: InsertTextFormat.Snippet, + }); + } + } + + // proposals for values + const types: { [type: string]: boolean } = {}; + this.getValueCompletions(documentContext, currentDoc, offset, collector, types, node); + } catch (err) { + console.error(err); + } + + this.finalizeParentCompletion(result); + + result.items = this.mergeCompletionItems(result.items); + + // console.debug("COMPLETION RESULT:", result); + return result; + } + + /** + * Returns a new list of completion items with unique labels. + * If the label is the same, the information is merged. + * @param items The list of completion items to merge + * @returns A list of completion items with unique labels + */ + mergeCompletionItems(items: CompletionItem[]): CompletionItem[] { + const uniqueItems: CompletionItem[] = []; + const existingItems: { [key: string]: CompletionItem } = {}; + + items.forEach((item) => { + const key = `${item.label}-${item.insertText}`; + if (!existingItems[key]) { + existingItems[key] = item; + uniqueItems.push(item); + } else { + const existingItem = existingItems[key]; + if (item.documentation && existingItem.documentation) { + existingItem.documentation = this.mergeMarkupContent(existingItem.documentation, item.documentation); + } + } + }); + + return uniqueItems; + } + + /** + * Merges two MarkupContent objects into one. + * @param existing The existing MarkupContent object + * @param newContent The new MarkupContent object + * @returns The merged MarkupContent object + */ + mergeMarkupContent(existing?: string | MarkupContent, newContent?: string | MarkupContent): MarkupContent { + const existingContent = this.getMarkupContent(existing); + const newContentContent = this.getMarkupContent(newContent); + + if (!existingContent) { + return newContentContent; + } + + if (!newContentContent) { + return existingContent; + } + + return { + kind: MarkupKind.Markdown, + value: `${existingContent.value}\n\n${newContentContent.value}`, + }; + } + + /** + * Returns a MarkupContent object from a string or MarkupContent object. + * @param content The content to convert + * @returns The MarkupContent object + */ + getMarkupContent(content?: string | MarkupContent): MarkupContent { + if (!content) { + content = ""; + } + if (typeof content === "string") { + return { + kind: MarkupKind.Markdown, + value: content, + }; + } + return content; + } + + updateCompletionText(completionItem: CompletionItem, text: string): void { + completionItem.insertText = text; + if (completionItem.textEdit) { + completionItem.textEdit.newText = text; + } + } + + mergeSimpleInsertTexts( + label: string, + existingText: string, + addingText: string, + oneOfSchema: boolean + ): string | undefined { + const containsNewLineAfterColon = (value: string): boolean => { + return value.includes("\n"); + }; + const startWithNewLine = (value: string): boolean => { + return value.startsWith("\n"); + }; + const isNullObject = (value: string): boolean => { + const index = value.indexOf("\n"); + return index > 0 && value.substring(index, value.length).trim().length === 0; + }; + if (containsNewLineAfterColon(existingText) || containsNewLineAfterColon(addingText)) { + //if the exisiting object null one then replace with the non-null object + if (oneOfSchema && isNullObject(existingText) && !isNullObject(addingText) && !startWithNewLine(addingText)) { + return addingText; + } + return undefined; + } + const existingValues = this.getValuesFromInsertText(existingText); + const addingValues = this.getValuesFromInsertText(addingText); + + const newValues = Array.prototype.concat(existingValues, addingValues); + if (!newValues.length) { + return undefined; + } else if (newValues.length === 1) { + return `${label}: \${1:${newValues[0]}}`; + } else { + return `${label}: \${1|${newValues.join(",")}|}`; + } + } + + getValuesFromInsertText(insertText: string): string[] { + const value = insertText.substring(insertText.indexOf(":") + 1).trim(); + if (!value) { + return []; + } + const valueMath = value.match(/^\${1[|:]([^|]*)+\|?}$/); // ${1|one,two,three|} or ${1:one} + if (valueMath) { + return valueMath[1].split(","); + } + return [value]; + } + + private getInsertTextForProperty( + key: string, + propertySchema: JSONSchema | null, + separatorAfter: string, + indent = this.indentation + ): string { + const propertyText = this.getInsertTextForValue(key, "", "string"); + const resultText = propertyText + ":"; + + let value: string = ""; + let nValueProposals = 0; + if (propertySchema) { + let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; + if (!type) { + if (propertySchema.properties) { + type = "object"; + } else if (propertySchema.items) { + type = "array"; + } else if (propertySchema.anyOf) { + type = "anyOf"; + } + } + + if (propertySchema.enum) { + if (!value && propertySchema.enum.length === 1) { + value = " " + this.getInsertTextForGuessedValue(propertySchema.enum[0], "", type); + } + nValueProposals += propertySchema.enum.length; + } + + if (propertySchema.const) { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.const, "", type); + value = evaluateTab1Symbol(value); // prevent const being selected after snippet insert + value = " " + value; + } + nValueProposals++; + } + + if (isDefined(propertySchema.default)) { + if (!value) { + value = " " + this.getInsertTextForGuessedValue(propertySchema.default, "", type); + } + nValueProposals++; + } + + if (propertySchema.properties) { + return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, indent).insertText}`; + } else if (propertySchema.items) { + return `${resultText}\n${indent}- ${ + this.getInsertTextForArray(propertySchema.items, separatorAfter, 1, indent).insertText + }`; + } + if (nValueProposals === 0) { + switch (type) { + case "boolean": + value = " $1"; + break; + case "string": + value = " $1"; + break; + case "object": + value = `\n${indent}`; + break; + case "array": + value = `\n${indent}- `; + break; + case "number": + case "integer": + value = " ${1:0}"; + break; + case "null": + value = " ${1:null}"; + break; + case "anyOf": + value = " $1"; + break; + default: + return resultText; + } + } + } + if (!value || nValueProposals > 1) { + value = " $1"; + } + return resultText + value + separatorAfter; + } + + private getInsertTextForObject( + schema: JSONSchema, + separatorAfter: string, + indent = this.indentation, + insertIndex = 1 + ): InsertText { + let insertText = ""; + if (!schema.properties) { + insertText = `${indent}$${insertIndex++}\n`; + return { insertText, insertIndex }; + } + + const properties = schema.properties; + + if (!properties) { + return { insertText, insertIndex }; + } + + Object.keys(properties).forEach((key: string) => { + const propertySchema = properties[key]; + let type = Array.isArray(propertySchema?.type) ? propertySchema.type[0] : propertySchema?.type; + if (!type) { + if (propertySchema.anyOf) { + type = "anyOf"; + } + if (propertySchema.properties) { + type = "object"; + } + if (propertySchema.items) { + type = "array"; + } + } + if (schema.required && schema.required.indexOf(key) > -1) { + switch (type) { + case "boolean": + case "string": + case "number": + case "integer": + case "anyOf": { + let value = propertySchema.default || propertySchema.const; + if (value) { + if (type === "string") { + value = convertToStringValue(value); + } + insertText += `${indent}${key}: \${${insertIndex++}:${value}}\n`; + } else { + insertText += `${indent}${key}: $${insertIndex++}\n`; + } + break; + } + case "array": + { + const arrayInsertResult = this.getInsertTextForArray( + propertySchema.items, + separatorAfter, + insertIndex++, + indent + ); + const arrayInsertLines = arrayInsertResult.insertText.split("\n"); + let arrayTemplate = arrayInsertResult.insertText; + if (arrayInsertLines.length > 1) { + for (let index = 1; index < arrayInsertLines.length; index++) { + const element = arrayInsertLines[index]; + arrayInsertLines[index] = ` ${element}`; + } + arrayTemplate = arrayInsertLines.join("\n"); + } + insertIndex = arrayInsertResult.insertIndex; + insertText += `${indent}${key}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + } + break; + case "object": + { + const objectInsertResult = this.getInsertTextForObject( + propertySchema, + separatorAfter, + `${indent}${this.indentation}`, + insertIndex++ + ); + insertIndex = objectInsertResult.insertIndex; + insertText += `${indent}${key}:\n${objectInsertResult.insertText}\n`; + } + break; + } + } else if (propertySchema.default !== undefined) { + switch (type) { + case "boolean": + case "number": + case "integer": + insertText += `${indent}${ + //added quote if key is null + key === "null" ? this.getInsertTextForValue(key, "", "string") : key + }: \${${insertIndex++}:${propertySchema.default}}\n`; + break; + case "string": + insertText += `${indent}${key}: \${${insertIndex++}:${convertToStringValue(propertySchema.default)}}\n`; + break; + case "array": + case "object": + // TODO: support default value for array object + break; + } + } + }); + if (insertText.trim().length === 0) { + insertText = `${indent}$${insertIndex++}\n`; + } + insertText = insertText.trimRight() + separatorAfter; + return { insertText, insertIndex }; + } + + private createTempObjNode(currentWord: string, node: Node, currentDoc: YAMLSubDocument, range?: YamlRange): YAMLMap { + range = range || node.range || undefined; + const obj: { [key: string]: unknown } = {}; // Add index signature to allow indexing with a string + obj[currentWord] = null; + const map: YAMLMap = currentDoc.internalDocument.createNode(obj) as YAMLMap; + //********************************** + //TODO: the range here is not correct, it should be the range of the current line + //********************************** + map.range = range; + (map.items[0].key as Node).range = range; + (map.items[0].value as Node).range = range; + return map; + } + + private getInsertTextForArray( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + separatorAfter: string, + insertIndex = 1, + indent = this.indentation + ): InsertText { + let insertText = ""; + if (!schema) { + insertText = `$${insertIndex++}`; + return { insertText, insertIndex }; + } + let type = Array.isArray(schema.type) ? schema.type[0] : schema.type; + if (!type) { + if (schema.properties) { + type = "object"; + } + if (schema.items) { + type = "array"; + } + } + switch (schema.type) { + case "boolean": + insertText = `\${${insertIndex++}:false}`; + break; + case "number": + case "integer": + insertText = `\${${insertIndex++}:0}`; + break; + case "string": + insertText = `\${${insertIndex++}:""}`; + break; + case "object": + { + const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, `${indent} `, insertIndex++); + insertText = objectInsertResult.insertText.trimLeft(); + insertIndex = objectInsertResult.insertIndex; + } + break; + } + return { insertText, insertIndex }; + } + + private finalizeParentCompletion(result: CompletionList): void { + const reindexText = (insertTexts: string[]): string[] => { + //modify added props to have unique $x + let max$index = 0; + return insertTexts.map((text) => { + const match = text.match(/\$([0-9]+)|\${[0-9]+:/g); + if (!match) { + return text; + } + const max$indexLocal = match + .map((m) => +m.replace(/\${([0-9]+)[:|]/g, "$1").replace("$", "")) // get numbers form $1 or ${1:...} + .reduce((p, n) => (n > p ? n : p), 0); // find the max one + const reindexedStr = text + .replace(/\$([0-9]+)/g, (s, args) => "$" + (+args + max$index)) // increment each by max$index + .replace(/\${([0-9]+)[:|]/g, (s, args) => "${" + (+args + max$index) + ":"); // increment each by max$index + max$index += max$indexLocal; + return reindexedStr; + }); + }; + + result.items.forEach((completionItem) => { + if (isParentCompletionItem(completionItem) && completionItem.parent) { + const indent = completionItem.parent.indent || ""; + + let insertText = completionItem.insertText || ""; + if (completionItem.parent.insertTexts) { + const reindexedTexts = reindexText(completionItem.parent.insertTexts); + + // add indent to each object property and join completion item texts + insertText = reindexedTexts.join(`\n${indent}`); + + // trim $1 from end of completion + if (insertText.endsWith("$1")) { + insertText = insertText.substring(0, insertText.length - 2); + } + + completionItem.insertText = this.arrayPrefixIndentation + insertText; + } + + if (completionItem.textEdit) { + completionItem.textEdit.newText = insertText; + } + // remove $x or use {$x:value} in documentation + const mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, ""); + + const originalDocumentation = completionItem.documentation + ? [completionItem.documentation, "", "----", ""] + : []; + completionItem.documentation = { + kind: MarkupKind.Markdown, + value: [...originalDocumentation, "```yaml", indent + mdText, "```"].join("\n"), + }; + delete completionItem.parent; + } + }); + } + + private async addPropertyCompletions( + documentContext: DocumentContext, + doc: YAMLSubDocument, + node: YAMLMap, + originalNode: YamlNode, + separatorAfter: string, + collector: CompletionsCollector, + textBuffer: TextBuffer, + overwriteRange: Range + ): Promise { + const didCallFromAutoComplete = true; + const nodeOffset = textBuffer.getOffsetAt(overwriteRange.start); + let matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, nodeOffset, didCallFromAutoComplete); + const existingKey = textBuffer.getText(overwriteRange); + const lineContent = textBuffer.getLineContent(overwriteRange.start.line); + const hasOnlyWhitespace = lineContent.trim().length === 0; + const hasColon = lineContent.indexOf(":") !== -1; + // const isInArray = lineContent.trimLeft().indexOf("-") === 0; + const nodeParent = doc.getParent(node); + const matchOriginal = matchingSchemas.find(function (it) { + return it.node.internalNode === originalNode && it.schema.properties; + }); + + // if the parent is the `job` key, then we need to add the workflow inputs + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "job") { + this.workflowInputs.forEach((input) => { + collector.add({ + kind: CompletionItemKind.Property, + label: input.name, + insertText: `${this.quoteIfColon(input.name)}:`, + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup(input.doc), + }); + }); + return; + } + + // if the parent is the `outputs` key, then we need to add the workflow outputs + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "outputs") { + this.workflowOutputs.forEach((output) => { + collector.add({ + kind: CompletionItemKind.Property, + label: output.name, + insertText: `${this.quoteIfColon(output.name)}:`, + insertTextFormat: InsertTextFormat.Snippet, + }); + }); + return; + } + + //If the parent is a workflow input, then we need to add the properties from the document context + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key)) { + const nodeParentKey = nodeParent.key.value; + const matchingWorkflowInput = this.workflowInputs.find((input) => input.name === nodeParentKey); + if (matchingWorkflowInput) { + const type = matchingWorkflowInput.type; + const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; + switch (type) { + case "data": + case "File": + matchingSchemas = matchingSchemas.filter( + (schema) => schema.schema.title && DATA_INPUT_TYPE_OPTIONS.includes(schema.schema.title) + ); + if (node.items.length === 1 && isScalar(node.items[0].key) && node.items[0].key.value === "") { + for (const schema of matchingSchemas) { + const firstRequired = schema.schema.required?.find((key) => key !== "class") ?? ""; + collector.add( + { + kind: CompletionItemKind.Property, + label: `class ${schema.schema.title}`, + insertText: `class: File\n${firstRequired}: \${1:${firstRequired}}`, + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup( + `The class of the input. This type of input requires the \`${firstRequired}\` attribute.` + ), + sortText: `${DATA_INPUT_TYPE_OPTIONS.indexOf(schema.schema.title!)}`, + }, + false + ); + } + return; + } + break; + case "collection": + matchingSchemas = matchingSchemas.filter((schema) => schema.schema.title === "Collection"); + break; + } + } + } + + const oneOfSchema = matchingSchemas + .filter((schema) => schema.schema.oneOf) + .map((oneOfSchema) => oneOfSchema.schema.oneOf)[0]; + let didOneOfSchemaMatches = false; + if (oneOfSchema?.length ?? 0 < matchingSchemas.length) { + oneOfSchema?.forEach((property: JSONSchema, index: number) => { + if ( + !matchingSchemas[index]?.schema.oneOf && + matchingSchemas[index]?.schema.properties === property.properties + ) { + didOneOfSchemaMatches = true; + } + }); + } + + for (const schema of matchingSchemas) { + const internalNode = schema.node.internalNode as HasRange; + if ( + (rangeMatches(internalNode, node as HasRange) && !matchOriginal) || + (internalNode === originalNode && !hasColon) || + (schema.node.parent?.internalNode === originalNode && !hasColon) + ) { + const schemaProperties = schema.schema.properties; + if (schemaProperties) { + const maxProperties = schema.schema.maxProperties; + if ( + maxProperties === undefined || + node.items === undefined || + node.items.length < maxProperties || + (node.items.length === maxProperties && !hasOnlyWhitespace) + ) { + for (const key in schemaProperties) { + if (Object.prototype.hasOwnProperty.call(schemaProperties, key)) { + const propertySchema = schemaProperties[key]; + + if ( + typeof propertySchema === "object" && + !propertySchema.deprecationMessage && + !propertySchema["doNotSuggest"] + ) { + let identCompensation = ""; + if (node.range && nodeParent && isSeq(nodeParent) && node.items.length <= 1 && !hasOnlyWhitespace) { + // because there is a slash '-' to prevent the properties generated to have the correct + // indent + const sourceText = textBuffer.getText(); + const indexOfSlash = sourceText.lastIndexOf("-", node.range[0] - 1); + if (indexOfSlash >= 0) { + // add one space to compensate the '-' + const overwriteChars = overwriteRange.end.character - overwriteRange.start.character; + identCompensation = " " + sourceText.slice(indexOfSlash + 1, node.range[1] - overwriteChars); + } + } + identCompensation += this.arrayPrefixIndentation; + + // if check that current node has last pair with "null" value and key witch match key from schema, + // and if schema has array definition it add completion item for array item creation + let pair: Pair | undefined; + if ( + propertySchema.type === "array" && + (pair = node.items.find( + (it) => + isScalar(it.key) && + it.key.range && + it.key.value === key && + isScalar(it.value) && + !it.value.value && + textBuffer.getPosition(it.key.range[2]).line === overwriteRange.end.line - 1 + )) && + pair + ) { + if (Array.isArray(propertySchema.items)) { + this.addSchemaValueCompletions( + propertySchema.items[0], + separatorAfter, + collector, + {}, + "property" + ); + } else if (typeof propertySchema.items === "object" && propertySchema.items.type === "object") { + this.addArrayItemValueCompletion(propertySchema.items, separatorAfter, collector); + } + } + + let insertText = key; + if (!key.startsWith(existingKey) || !hasColon) { + insertText = this.getInsertTextForProperty( + key, + propertySchema, + separatorAfter, + identCompensation + this.indentation + ); + } + const isNodeNull = + (isScalar(originalNode) && originalNode.value === null) || + (isMap(originalNode) && originalNode.items.length === 0); + const existsParentCompletion = schema.schema.required?.length ?? 0 > 0; + if (!isNodeNull || !existsParentCompletion) { + collector.add( + { + kind: CompletionItemKind.Property, + label: key, + insertText, + insertTextFormat: InsertTextFormat.Snippet, + documentation: propertySchema.description || "", + }, + didOneOfSchemaMatches + ); + } + // if the prop is required add it also to parent suggestion + if (schema.schema.required?.includes(key)) { + collector.add({ + label: key, + insertText: this.getInsertTextForProperty( + key, + propertySchema, + separatorAfter, + identCompensation + this.indentation + ), + insertTextFormat: InsertTextFormat.Snippet, + documentation: propertySchema.description || "", + parent: { + schema: schema.schema, + indent: identCompensation, + }, + }); + } + } + } + } + } + } + // Error fix + // If this is a array of string/boolean/number + // test: + // - item1 + // it will treated as a property key since `:` has been appended + if (nodeParent && isSeq(nodeParent) && isPrimitiveType(schema.schema)) { + this.addSchemaValueCompletions( + schema.schema, + separatorAfter, + collector, + {}, + "property", + Array.isArray(nodeParent.items) + ); + } + } + } + } + + private getValueCompletions( + documentContext: DocumentContext, + doc: YAMLSubDocument, + offset: number, + collector: CompletionsCollector, + types: { [type: string]: boolean }, + node?: YamlNode + ): void { + const schema = this.schemaService.schema; + let parentKey: string | null = null; + + if (node && isScalar(node)) { + node = doc.getParent(node); + } + + if (!node) { + this.addSchemaValueCompletions(schema.schema, "", collector, types, "value"); + return; + } + + if (isPair(node)) { + const valueNode: Node = node.value as Node; + if (valueNode && valueNode.range && offset > valueNode.range[0] + valueNode.range[2]) { + return; // we are past the value node + } + parentKey = isScalar(node.key) ? node.key.value + "" : null; + node = doc.getParent(node); + } + + if (node && (parentKey !== null || isSeq(node))) { + const separatorAfter = ""; + const didCallFromAutoComplete = true; + // Check if the parent is a workflow input + const matchingInput = this.workflowInputs.find((input) => input.name === parentKey); + if (matchingInput) { + const type = matchingInput.type; + let typeSchema: JSONSchema = { type: "string" }; + switch (type) { + case "boolean": + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); + return; + case "null": + this.addNullValueCompletion(separatorAfter, collector); + return; + case "double": + case "float": + case "long": + case "int": + case "integer": + typeSchema = { type: "number" }; + break; + } + this.addSchemaValueCompletions(typeSchema, separatorAfter, collector, types, "value"); + return; + } + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, offset, didCallFromAutoComplete); + for (const s of matchingSchemas) { + const internalNode = s.node.internalNode as HasRange; + if (rangeMatches(internalNode, node as HasRange) && s.schema) { + if (s.schema.items) { + if (isSeq(node) && node.items) { + if (Array.isArray(s.schema.items)) { + const index = this.findItemAtOffset(node, offset); + if (index < s.schema.items.length) { + this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, "value"); + } + } else if ( + typeof s.schema.items === "object" && + (s.schema.items.type === "object" || isAnyOfAllOfOneOfType(s.schema.items)) + ) { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, "value", true); + } else { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, "value"); + } + } + } + if (s.schema.properties && parentKey !== null) { + const propertySchema = s.schema.properties[parentKey]; + if (propertySchema) { + this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types, "value"); + } + } else if (s.schema.additionalProperties) { + this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types, "value"); + } + } + } + + if (types["boolean"]) { + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); + } + if (types["null"]) { + this.addNullValueCompletion(separatorAfter, collector); + } + } + } + + private addArrayItemValueCompletion( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + index?: number + ): void { + const schemaType = getSchemaTypeName(schema); + const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimStart()}`; + //append insertText to documentation + const schemaTypeTitle = schemaType ? " type `" + schemaType + "`" : ""; + const schemaDescription = schema.description ? " (" + schema.description + ")" : ""; + const documentation = this.getDocumentationWithMarkdownText( + `Create an item of an array${schemaTypeTitle}${schemaDescription}`, + insertText + ); + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: "- (array item) " + (schemaType || index), + documentation: documentation, + insertText: insertText, + insertTextFormat: InsertTextFormat.Snippet, + }); + } + + private getDocumentationWithMarkdownText(documentation: string, insertText: string): string | MarkupContent { + let res: string | MarkupContent = documentation; + if (this.doesSupportMarkdown()) { + insertText = insertText + .replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => { + return arg; + }) + .replace(/\$([0-9]+)/g, ""); + res = this.fromMarkup(`${documentation}\n \`\`\`\n${insertText}\n\`\`\``) as MarkupContent; + } + return res; + } + + private fromMarkup(markupString: string): MarkupContent | undefined { + if (markupString && this.doesSupportMarkdown()) { + return { + kind: MarkupKind.Markdown, + value: markupString, + }; + } + return undefined; + } + + private quoteIfColon(value: string): string { + return value.includes(":") ? `'${value}'` : `${value}`; + } + + private doesSupportMarkdown(): boolean { + // Forcing markdown for now + return true; + } + + private getInsertTextForPlainText(text: string): string { + return text.replace(/[\\$}]/g, "\\$&"); // escape $, \ and } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getInsertTextForValue(value: any, separatorAfter: string, type?: string | string[]): string { + if (value === null) { + return "null"; // replace type null with string 'null' + } + switch (typeof value) { + case "object": { + const indent = this.indentation; + return this.getInsertTemplateForValue(value, indent, { index: 1 }, separatorAfter); + } + case "number": + case "boolean": + return this.getInsertTextForPlainText(value + separatorAfter); + } + type = Array.isArray(type) ? type[0] : type; + if (type === "string") { + value = convertToStringValue(value); + } + return this.getInsertTextForPlainText(value + separatorAfter); + } + + private getInsertTemplateForValue( + value: unknown | [], + indent: string, + navOrder: { index: number }, + separatorAfter: string + ): string { + if (Array.isArray(value)) { + let insertText = "\n"; + for (const arrValue of value) { + insertText += `${indent}- \${${navOrder.index++}:${arrValue}}\n`; + } + return insertText; + } else if (typeof value === "object") { + let insertText = "\n"; + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const element = value[key as keyof typeof value]; + insertText += `${indent}\${${navOrder.index++}:${key}}:`; + let valueTemplate; + if (typeof element === "object") { + valueTemplate = `${this.getInsertTemplateForValue( + element, + indent + this.indentation, + navOrder, + separatorAfter + )}`; + } else { + valueTemplate = ` \${${navOrder.index++}:${this.getInsertTextForPlainText(element + separatorAfter)}}\n`; + } + insertText += `${valueTemplate}`; + } + } + return insertText; + } + return this.getInsertTextForPlainText(value + separatorAfter); + } + + private addSchemaValueCompletions( + schema: JSONSchemaRef, + separatorAfter: string, + collector: CompletionsCollector, + types: { [key: string]: boolean }, + completionType: "property" | "value", + isArray?: boolean + ): void { + if (typeof schema === "object") { + this.addEnumValueCompletions(schema, separatorAfter, collector, isArray); + this.addDefaultValueCompletions(schema, separatorAfter, collector); + this.collectTypes(schema, types); + + if (isArray && completionType === "value" && !isAnyOfAllOfOneOfType(schema)) { + // add array only for final types (no anyOf, allOf, oneOf) + this.addArrayItemValueCompletion(schema, separatorAfter, collector); + } + + if (Array.isArray(schema.allOf)) { + schema.allOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + if (Array.isArray(schema.anyOf)) { + schema.anyOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + if (Array.isArray(schema.oneOf)) { + schema.oneOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + } + } + + private collectTypes(schema: JSONSchema, types: { [key: string]: boolean }): void { + if (Array.isArray(schema.enum) || isDefined(schema.const)) { + return; + } + const type = schema.type; + if (Array.isArray(type)) { + type.forEach(function (t) { + return (types[t] = true); + }); + } else if (type) { + types[type] = true; + } + } + + private addDefaultValueCompletions( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + arrayDepth = 0 + ): void { + let hasProposals = false; + if (isDefined(schema.default)) { + let type = schema.type; + let value = schema.default; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = "array"; + } + let label; + if (typeof value == "object") { + label = "Default value"; + } else { + label = value.toString().replace(doubleQuotesEscapeRegExp, '"'); + } + collector.add({ + kind: this.getSuggestionKind(type), + label, + insertText: this.getInsertTextForValue(value, separatorAfter, type), + insertTextFormat: InsertTextFormat.Snippet, + detail: "Default value", + }); + hasProposals = true; + } + + if (!hasProposals && typeof schema.items === "object" && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); + } + } + + private addEnumValueCompletions( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + isArray?: boolean + ): void { + if (isDefined(schema.const) && !isArray) { + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(schema.const), + insertText: this.getInsertTextForValue(schema.const, separatorAfter, schema.type), + insertTextFormat: InsertTextFormat.Snippet, + documentation: schema.description, + }); + } + if (Array.isArray(schema.enum)) { + for (let i = 0, length = schema.enum.length; i < length; i++) { + const enm = schema.enum[i]; + let documentation = schema.description; + if (schema.enumDescriptions && i < schema.enumDescriptions.length) { + documentation = schema.enumDescriptions[i]; + } + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(enm), + insertText: this.getInsertTextForValue(enm, separatorAfter, schema.type), + insertTextFormat: InsertTextFormat.Snippet, + documentation: documentation, + }); + } + } + } + + private getLabelForValue(value: unknown): string { + if (value === null) { + return "null"; // return string with 'null' value if schema contains null as possible value + } + if (Array.isArray(value)) { + return JSON.stringify(value); + } + return "" + value; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getInsertTextForGuessedValue(value: any, separatorAfter: string, type?: string): string { + switch (typeof value) { + case "object": + if (value === null) { + return "${1:null}" + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter, type); + case "string": { + let snippetValue = JSON.stringify(value); + snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes + snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + if (type === "string") { + snippetValue = convertToStringValue(snippetValue); + } + return "${1:" + snippetValue + "}" + separatorAfter; + } + case "number": + case "boolean": + return "${1:" + value + "}" + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter, type); + } + + private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind("boolean"), + label: value ? "true" : "false", + insertText: this.getInsertTextForValue(value, separatorAfter, "boolean"), + insertTextFormat: InsertTextFormat.Snippet, + documentation: "", + }); + } + + private addNullValueCompletion(separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind("null"), + label: "null", + insertText: "null" + separatorAfter, + insertTextFormat: InsertTextFormat.Snippet, + documentation: "", + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getSuggestionKind(type: any): CompletionItemKind { + if (Array.isArray(type)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const array = type; + type = array.length > 0 ? array[0] : null; + } + if (!type) { + return CompletionItemKind.Value; + } + switch (type) { + case "string": + return CompletionItemKind.Value; + case "object": + return CompletionItemKind.Module; + case "property": + return CompletionItemKind.Property; + default: + return CompletionItemKind.Value; + } + } + + private findItemAtOffset(seqNode: YAMLSeq, offset: number): number { + for (let i = seqNode.items.length - 1; i >= 0; i--) { + const node = seqNode.items[i]; + if (isNode(node)) { + if (node.range) { + if (offset > node.range[1]) { + return i; + } else if (offset >= node.range[0]) { + return i; + } + } + } + } + + return 0; + } +} + +/** + * simplify `{$1:value}` to `value` + */ +function evaluateTab1Symbol(value: string): string { + return value.replace(/\$\{1:(.*)\}/, "$1"); +} + +function isParentCompletionItem(item: CompletionItemBase): item is CompletionItem { + return "parent" in item; +} + +function convertToStringValue(param: unknown): string { + const isNumberExp = /^\d+$/; + let value: string; + if (typeof param === "string") { + value = param; + } else { + value = "" + param; + } + if (value.length === 0) { + return value; + } + + if (value === "true" || value === "false" || value === "null" || isNumberExp.test(value)) { + return `"${value}"`; + } + + if (value.indexOf('"') !== -1) { + value = value.replace(doubleQuotesEscapeRegExp, '"'); + } + + let doQuote = !isNaN(parseInt(value)) || value.charAt(0) === "@"; + + if (!doQuote) { + // need to quote value if in `foo: bar`, `foo : bar` (mapping) or `foo:` (partial map) format + // but `foo:bar` and `:bar` (colon without white-space after it) are just plain string + let idx = value.indexOf(":", 0); + for (; idx > 0 && idx < value.length; idx = value.indexOf(":", idx + 1)) { + if (idx === value.length - 1) { + // `foo:` (partial map) format + doQuote = true; + break; + } + + // there are only two valid kinds of white-space in yaml: space or tab + // ref: https://yaml.org/spec/1.2.1/#id2775170 + const nextChar = value.charAt(idx + 1); + if (nextChar === "\t" || nextChar === " ") { + doQuote = true; + break; + } + } + } + + if (doQuote) { + value = `"${value}"`; + } + + return value; +} + +export function isPrimitiveType(schema: JSONSchema): boolean { + return schema.type !== "object" && !isAnyOfAllOfOneOfType(schema); +} + +export function isAnyOfAllOfOneOfType(schema: JSONSchema): boolean { + return !!(schema.anyOf || schema.allOf || schema.oneOf); +} + +export function getSchemaTypeName(schema: JSONSchema): string { + const closestTitleWithType = schema.type; + if (schema.title) { + return schema.title; + } + if (schema.$id) { + return getSchemaRefTypeTitle(schema.$id); + } + if (schema.$ref) { + return getSchemaRefTypeTitle(schema.$ref); + } + return ( + (Array.isArray(schema.type) + ? schema.type.join(" | ") + : closestTitleWithType + ? schema.type?.concat("(", schema.title ?? "Unknown", ")") + : schema.type ?? "Unknown") ?? "Unknown" + ); +} + +export function getSchemaRefTypeTitle($ref: string): string { + const match = $ref.match(/^(?:.*\/)?(.*?)(?:\.schema\.json)?$/); + let type = !!match && match[1]; + if (!type) { + type = "typeNotFound"; + console.error(`$ref (${$ref}) not parsed properly`); + } + return type; +} diff --git a/server/packages/workflow-tests-language-service/src/services/completion/index.ts b/server/packages/workflow-tests-language-service/src/services/completion/index.ts new file mode 100644 index 0000000..f12a8a1 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/index.ts @@ -0,0 +1,3 @@ +import { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl } from "./completion"; + +export { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl }; diff --git a/server/packages/workflow-tests-language-service/src/services/hover.ts b/server/packages/workflow-tests-language-service/src/services/hover.ts new file mode 100644 index 0000000..9e9205a --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/hover.ts @@ -0,0 +1,199 @@ +import { ASTNode } from "@gxwf/server-common/src/ast/types"; +import { + DocumentContext, + Hover, + MarkupContent, + MarkupKind, + Position, + Range, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { isAllSchemasMatched, isBoolean } from "../schema/adapter"; +import { JSONSchemaRef } from "../schema/jsonSchema"; +import { WorkflowTestsSchemaService } from "../schema/service"; +import { TYPES } from "../types"; + +export interface WorkflowTestsHoverService { + doHover(documentContext: DocumentContext, position: Position): Promise; +} + +@injectable() +export class WorkflowTestsHoverServiceImpl implements WorkflowTestsHoverService { + constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) {} + + public async doHover(documentContext: DocumentContext, position: Position): Promise { + const offset = documentContext.textDocument.offsetAt(position); + let node = documentContext.nodeManager.getNodeFromOffset(offset); + if ( + !node || + ((node.type === "object" || node.type === "array") && + offset > node.offset + 1 && + offset < node.offset + node.length - 1) + ) { + return Promise.resolve(null); + } + const hoverRangeNode = node; + + // use the property description when hovering over an object key + if (node.type === "string") { + const parent = node.parent; + if (parent && parent.type === "property" && parent.keyNode === node) { + node = parent.valueNode; + } + } + + if (!node) { + return Promise.resolve(null); + } + + const hoverRange = Range.create( + documentContext.textDocument.positionAt(hoverRangeNode.offset), + documentContext.textDocument.positionAt(hoverRangeNode.offset + hoverRangeNode.length) + ); + + if (this.parentPropertyMatchesKey(node, "job")) { + const inputHover = await this.getHoverForWorkflowInput(documentContext, node, hoverRange); + if (inputHover) { + return inputHover; + } + } + + if (this.parentPropertyMatchesKey(node, "outputs")) { + const outputHover = await this.getHoverForWorkflowOutput(documentContext, node, hoverRange); + if (outputHover) { + return outputHover; + } + } + + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, node.offset); + + const removePipe = (value: string): string => { + return value.replace(/\|\|\s*$/, ""); + }; + + let title: string | undefined = undefined; + let markdownDescription: string | undefined = undefined; + + matchingSchemas.every((matchingSchema) => { + if ( + (matchingSchema.node === node || (node?.type === "property" && node.valueNode === matchingSchema.node)) && + matchingSchema.schema + ) { + title = title || matchingSchema.schema.title; + markdownDescription = markdownDescription || matchingSchema.schema.description; + if (matchingSchema.schema.anyOf && isAllSchemasMatched(node, matchingSchemas, matchingSchema.schema)) { + title = ""; + markdownDescription = ""; + matchingSchema.schema.anyOf.forEach((childSchema: JSONSchemaRef, index: number) => { + if (isBoolean(childSchema)) { + return; + } + title += childSchema.title || ""; + markdownDescription += childSchema.description || ""; + const numOptions = matchingSchema.schema.anyOf ? matchingSchema.schema.anyOf.length - 1 : 0; + if (index !== numOptions) { + title += " || "; + markdownDescription += " || "; + } + }); + title = removePipe(title); + markdownDescription = removePipe(markdownDescription); + } + } + return true; + }); + let result = ""; + if (title) { + result = `#### ${title}`; + } + if (markdownDescription) { + if (result.length > 0) { + result += "\n\n"; + } + result += markdownDescription; + } + + const contents = [result == "" ? "Nothing found" : result]; + const hover = this.createHover(contents.join("\n\n"), hoverRange); + return Promise.resolve(hover); + } + + private createHover(contents: string, hoverRange: Range): Hover { + const markupContent: MarkupContent = { + kind: MarkupKind.Markdown, + value: contents, + }; + const result: Hover = { + contents: markupContent, + range: hoverRange, + }; + return result; + } + + private parentPropertyMatchesKey(node: ASTNode, key: string): boolean { + // The first parent is the value node (object), the second parent is the property node + // we are looking for. + // ParentNode (property) <- Target node + // |- ValueNode (object) + // |- Node (property) <- Initial node + const parent = node.parent?.parent; + if (!parent || parent.type !== "property") { + return false; + } + return parent.keyNode.value === key; + } + + private async getHoverForWorkflowInput( + documentContext: DocumentContext, + node: ASTNode, + hoverRange: Range + ): Promise { + if (node.type !== "property") { + return null; + } + const key = node.keyNode.value; + const testDocument = documentContext as WorkflowTestsDocument; + const inputs = await testDocument.getWorkflowInputs(); + const matchingInput = inputs.find((input) => input.name === key); + if (matchingInput) { + const hoverContents = [`**${matchingInput.name}** (Input)`]; + if (matchingInput.doc) { + hoverContents.push(matchingInput.doc); + } + if (matchingInput.type) { + hoverContents.push(`Type: ${matchingInput.type}`); + } + return this.createHover(hoverContents.join("\n\n"), hoverRange); + } + return this.createHover("Input not found", hoverRange); + } + + private async getHoverForWorkflowOutput( + documentContext: DocumentContext, + node: ASTNode, + hoverRange: Range + ): Promise { + if (node.type !== "property") { + return null; + } + const key = node.keyNode.value; + const testDocument = documentContext as WorkflowTestsDocument; + const outputs = await testDocument.getWorkflowOutputs(); + const matchingOutput = outputs.find((output) => output.name === key); + if (matchingOutput) { + const hoverContents = [`**${matchingOutput.name}** (Output)`]; + if (matchingOutput.doc) { + hoverContents.push(matchingOutput.doc); + } + if (matchingOutput.type) { + hoverContents.push(`Type: ${matchingOutput.type}`); + } + if (matchingOutput.uuid) { + hoverContents.push(matchingOutput.uuid); + } + return this.createHover(hoverContents.join("\n\n"), hoverRange); + } + return this.createHover("Output not found", hoverRange); + } +} diff --git a/server/packages/workflow-tests-language-service/src/services/validation.ts b/server/packages/workflow-tests-language-service/src/services/validation.ts new file mode 100644 index 0000000..5e24874 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/validation.ts @@ -0,0 +1,131 @@ +import { + Diagnostic, + DiagnosticSeverity, + DocumentContext, + Range, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { ResolvedSchema } from "../schema/jsonSchema"; +import { WorkflowTestsSchemaProvider } from "../schema/provider"; +import { WorkflowTestsSchemaService } from "../schema/service"; +import { TYPES } from "../types"; + +export interface WorkflowTestsValidationService { + doValidation(documentContext: DocumentContext): Promise; +} + +@injectable() +export class WorkflowTestsValidationServiceImpl implements WorkflowTestsValidationService { + constructor( + @inject(TYPES.WorkflowTestsSchemaProvider) protected schemaProvider: WorkflowTestsSchemaProvider, + @inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService + ) {} + + async doValidation(documentContext: DocumentContext): Promise { + const diagnostics: Diagnostic[] = []; + const added: { [signature: string]: boolean } = {}; + + const addProblem = (problem: Diagnostic): void => { + const signature = `${problem.range.start.line} ${problem.range.start.character} ${problem.message}`; + if (!added[signature]) { + added[signature] = true; + diagnostics.push(problem); + } + }; + const getDiagnostics = (schema: ResolvedSchema | undefined): Diagnostic[] => { + const severity = DiagnosticSeverity.Error; + + if (schema) { + const addSchemaProblem = (errorMessage: string): void => { + if (documentContext.nodeManager.root) { + const astRoot = documentContext.nodeManager.root; + const property = astRoot.type === "object" ? astRoot.properties[0] : undefined; + if (property && property.keyNode.value === "$schema") { + const node = property.valueNode || property; + const range = Range.create( + documentContext.textDocument.positionAt(node.offset), + documentContext.textDocument.positionAt(node.offset + node.length) + ); + addProblem(Diagnostic.create(range, errorMessage, severity)); + } else { + const range = Range.create( + documentContext.textDocument.positionAt(astRoot.offset), + documentContext.textDocument.positionAt(astRoot.offset + 1) + ); + addProblem(Diagnostic.create(range, errorMessage, severity)); + } + } + }; + + if (schema.errors.length) { + addSchemaProblem(schema.errors[0]); + } else if (severity) { + const semanticErrors = this.schemaService.validate(documentContext, severity); + if (semanticErrors) { + semanticErrors.forEach(addProblem); + } + } + } + + return diagnostics; + }; + + const schema = this.schemaProvider.getResolvedSchema(); + const schemaValidation = getDiagnostics(schema); + const semanticValidation = await this.doSemanticValidation(documentContext); + return schemaValidation.concat(semanticValidation); + } + + async doSemanticValidation(documentContext: DocumentContext): Promise { + const testDocument = documentContext as WorkflowTestsDocument; + const inputDiagnostics = await this.validateWorkflowInputs(testDocument); + const outputDiagnostics = await this.validateWorkflowOutputs(testDocument); + return inputDiagnostics.concat(outputDiagnostics); + } + + private async validateWorkflowInputs(testDocument: WorkflowTestsDocument): Promise { + const diagnostics: Diagnostic[] = []; + const workflowInputs = await testDocument.getWorkflowInputs(); + const documentInputNodes = testDocument.nodeManager.getAllPropertyNodesByName("job")[0]?.valueNode?.children ?? []; + documentInputNodes.forEach((inputNode) => { + if (inputNode.type !== "property") { + return; + } + const inputName = inputNode.keyNode.value as string; + const input = workflowInputs.find((i) => i.name === inputName); + if (!input) { + const range = Range.create( + testDocument.textDocument.positionAt(inputNode.offset), + testDocument.textDocument.positionAt(inputNode.offset + inputNode.length) + ); + const message = `Input "${inputName}" is not defined in the associated workflow.`; + diagnostics.push(Diagnostic.create(range, message, DiagnosticSeverity.Error)); + } + }); + return diagnostics; + } + + private async validateWorkflowOutputs(testDocument: WorkflowTestsDocument): Promise { + const diagnostics: Diagnostic[] = []; + const workflowOutputs = await testDocument.getWorkflowOutputs(); + const documentOutputNodes = + testDocument.nodeManager.getAllPropertyNodesByName("outputs")[0]?.valueNode?.children ?? []; + documentOutputNodes.forEach((outputNode) => { + if (outputNode.type !== "property") { + return; + } + const outputName = outputNode.keyNode.value as string; + const output = workflowOutputs.find((o) => o.name === outputName); + if (!output) { + const range = Range.create( + testDocument.textDocument.positionAt(outputNode.offset), + testDocument.textDocument.positionAt(outputNode.offset + outputNode.length) + ); + const message = `Output "${outputName}" is not defined in the associated workflow.`; + diagnostics.push(Diagnostic.create(range, message, DiagnosticSeverity.Error)); + } + }); + return diagnostics; + } +} diff --git a/server/packages/workflow-tests-language-service/src/types.ts b/server/packages/workflow-tests-language-service/src/types.ts new file mode 100644 index 0000000..53efaf6 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/types.ts @@ -0,0 +1,8 @@ +export const TYPES = { + JSONSchemaService: Symbol.for("JSONSchemaService"), + WorkflowTestsSchemaProvider: Symbol.for("WorkflowTestsSchemaProvider"), + WorkflowTestsSchemaService: Symbol.for("WorkflowTestsSchemaService"), + WorkflowTestsHoverService: Symbol.for("WorkflowTestsHoverService"), + WorkflowTestsCompletionService: Symbol.for("WorkflowTestsCompletionService"), + WorkflowTestsValidationService: Symbol.for("WorkflowTestsValidationService"), +}; diff --git a/server/packages/workflow-tests-language-service/tests/testHelpers.ts b/server/packages/workflow-tests-language-service/tests/testHelpers.ts new file mode 100644 index 0000000..cc584f5 --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/testHelpers.ts @@ -0,0 +1,19 @@ +import { TextDocument, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { YAMLDocument, getLanguageService } from "@gxwf/yaml-language-service/src"; +import { GxWorkflowTestsDocument } from "../src/document"; + +export function toYamlDocument(contents: string): { textDoc: TextDocument; yamlDoc: YAMLDocument } { + const textDoc = TextDocument.create("foo://bar/file.gxwf-tests.yaml", "gxwftests", 0, contents); + + const ls = getLanguageService(); + const yamlDoc = ls.parseYAMLDocument(textDoc) as YAMLDocument; + return { textDoc, yamlDoc }; +} + +export function createGxWorkflowTestsDocument( + contents: string, + workflowDataProvider?: WorkflowDataProvider +): GxWorkflowTestsDocument { + const { textDoc, yamlDoc } = toYamlDocument(contents); + return new GxWorkflowTestsDocument(textDoc, yamlDoc, workflowDataProvider); +} diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts new file mode 100644 index 0000000..08266ca --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -0,0 +1,234 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { + CompletionItem, + CompletionList, + WorkflowDataProvider, + WorkflowInput, +} from "@gxwf/server-common/src/languageTypes"; +import { + EXPECTED_WORKFLOW_INPUTS, + EXPECTED_WORKFLOW_OUTPUTS, + FAKE_DATASET_INPUT, + FAKE_WORKFLOW_DATA_PROVIDER, + parseTemplate, +} from "@gxwf/server-common/tests/testHelpers"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsSchemaService } from "@gxwf/workflow-tests-language-service/src/schema/service"; +import { YAMLCompletionHelper } from "@gxwf/workflow-tests-language-service/src/services/completion/helper"; +import { TYPES } from "@gxwf/workflow-tests-language-service/src/types"; +import "reflect-metadata"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Completion Service", () => { + let helper: YAMLCompletionHelper; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + const schemaService = container.get(TYPES.WorkflowTestsSchemaService); + helper = new YAMLCompletionHelper(schemaService); + }); + + async function getCompletions( + contents: string, + position: { line: number; character: number }, + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER + ): Promise { + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); + + return await helper.doComplete(documentContext, position); + } + + it("should suggest the `New Workflow Test` when the document is empty", async () => { + const contents = ""; + const position = { line: 0, character: 0 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); + + it("should suggest the `New Workflow Test` when the document starts with dash", async () => { + const contents = "-"; + const position = { line: 0, character: 1 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); + + it("should suggest the `New Workflow Test` when the position is at the beginning of a new line", async () => { + const contents = "- doc:\n\n\n"; + const position = { line: 1, character: 0 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); + + it("should suggest the `job` and `outputs` entries when the position is at the same level as `doc`", async () => { + const expectedLabels = ["job", "outputs"]; + const template = ` +- doc: The docs + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(2); + for (const completionItem of completions!.items) { + expect(expectedLabels).toContain(completionItem.label); + } + }); + + it("should suggest the `job` entry as first suggestion when the position is at the Test definition level and starts with a `j`", async () => { + const template = ` +- doc: The docs + j$`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + const jobCompletion = completions!.items[0]!; + expect(jobCompletion.label).toBe("job"); + }); + + describe("Workflow Inputs Completion", () => { + it("should suggest all the defined inputs of the workflow when no inputs are defined in the test", async () => { + const template = ` +- doc: The docs + job: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(EXPECTED_WORKFLOW_INPUTS.length); + for (let index = 0; index < EXPECTED_WORKFLOW_INPUTS.length; index++) { + const workflowInput = EXPECTED_WORKFLOW_INPUTS[index]; + const completionItem = completions!.items[index]; + expectCompletionItemToMatchWorkflowInput(completionItem, workflowInput); + } + }); + + it("should suggest the input including quotes if the name contains colon", async () => { + const template = ` +- doc: The docs + job: + Input$`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + }); + + it("should not suggest an existing input when suggesting inputs", async () => { + const existingInput = FAKE_DATASET_INPUT; + const expectedNumOfRemainingInputs = EXPECTED_WORKFLOW_INPUTS.length - 1; + const template = ` +- doc: The docs + job: + ${existingInput.name}: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(expectedNumOfRemainingInputs); + const existingTestInput = completions?.items.find((item) => item.label === existingInput.name); + expect(existingTestInput).toBeUndefined(); + }); + + describe("Dataset Input Completions", () => { + it("should suggest the 3 possible File classes if there is nothing defined", async () => { + const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; + const datasetInput = FAKE_DATASET_INPUT; + const template = ` +- doc: The docs + job: + ${datasetInput.name}: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(3); + for (const completionItem of completions!.items) { + expect(completionItem.label).toContain("class"); + expect(completionItem.insertText).toContain("class: File"); + expect(DATA_INPUT_TYPE_OPTIONS).toContain(completionItem.label.replace("class ", "").trim()); + } + }); + + it("should suggest possible attributes for a (PathFile) File input", async () => { + const datasetInput = FAKE_DATASET_INPUT; + const expectedAttributes = ["name", "info", "dbkey", "filetype", "deferred"]; + const template = ` +- doc: The docs + job: + ${datasetInput.name}: + class: File + path: /path/to/file + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + for (const expectedAttribute of expectedAttributes) { + const completionItem = completions?.items.find((item) => item.label === expectedAttribute); + expect(completionItem).toBeDefined(); + } + }); + }); + describe("Dataset Output Completions", () => { + it("should suggest all the defined outputs of the workflow when no outputs are defined in the test", async () => { + const template = ` +- doc: The docs + outputs: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(EXPECTED_WORKFLOW_OUTPUTS.length); + for (let index = 0; index < EXPECTED_WORKFLOW_OUTPUTS.length; index++) { + const workflowOutput = EXPECTED_WORKFLOW_OUTPUTS[index]; + const completionItem = completions!.items[index]; + expect(completionItem.label).toEqual(workflowOutput.name); + } + }); + }); + }); +}); + +function expectCompletionItemDocumentationToContain(completionItem: CompletionItem, value: string): void { + expect(completionItem.documentation).toBeDefined(); + if (typeof completionItem.documentation === "string") { + expect(completionItem.documentation).toContain(value); + } else { + expect(completionItem.documentation?.value).toContain(value); + } +} + +function expectCompletionItemToMatchWorkflowInput(completionItem: CompletionItem, workflowInput: WorkflowInput): void { + expect(completionItem.label).toEqual(workflowInput.name); + expectCompletionItemDocumentationToContain(completionItem, workflowInput.doc); +} diff --git a/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts b/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts new file mode 100644 index 0000000..845756d --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts @@ -0,0 +1,132 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { Hover, MarkupContent, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { FAKE_WORKFLOW_DATA_PROVIDER, parseTemplate } from "@gxwf/server-common/tests/testHelpers"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsHoverService } from "@gxwf/workflow-tests-language-service/src/services/hover"; +import { TYPES } from "@gxwf/workflow-tests-language-service/src/types"; +import "reflect-metadata"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Hover Service", () => { + let service: WorkflowTestsHoverService; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + service = container.get(TYPES.WorkflowTestsHoverService); + }); + + async function getHover( + contents: string, + position: { line: number; character: number }, + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER + ): Promise { + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); + + return await service.doHover(documentContext, position); + } + + it("should return the documentation of the `doc` property when hovering over it", async () => { + const template = ` +- do$c: The docs + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Doc"); + expectHoverToContainContents(hover!, "Describes the purpose of the test"); + }); + + it("should return the documentation of the `job` property when hovering over it", async () => { + const template = ` +- doc: The docs + jo$b: + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Job"); + expectHoverToContainContents(hover!, "Defines job to execute"); + }); + + it("should return the documentation of the `outputs` property when hovering over it", async () => { + const template = ` +- doc: The docs + outp$uts: + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Outputs"); + expectHoverToContainContents(hover!, "Defines assertions about outputs"); + }); + + describe("Workflow Inputs/Outputs Hover", () => { + it.each<[string, string[]]>([ + [ + ` +- job: + My fake$ dataset: + `, + ["My fake dataset", "This is a simple dataset", "Type: data"], + ], + [ + ` +- job: + 'Input$ dataset: fake': + `, + ["Input dataset: fake", "This is a simple dataset with a colon in the name", "Type: File"], + ], + [ + ` +- job: + My fake$ collection: + `, + ["My fake collection", "This is a collection", "Type: collection"], + ], + [ + ` +- outputs: + My out$put: + `, + ["My output", "1234-5678-91011-1213"], + ], + [ + ` +- outputs: + My second out$put: + `, + ["My second output", "1234-5678-91011-1214"], + ], + [ + ` +- outputs: + 'My third out$put: with colon': + `, + ["My third output: with colon", "1234-5678-91011-1215"], + ], + ])( + "should return the documentation of the workflow inputs when hovering over them", + async (template: string, expectedHoverContents: string[]) => { + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + for (const expectedContent of expectedHoverContents) { + expectHoverToContainContents(hover!, expectedContent); + } + } + ); + }); +}); + +function expectHoverToContainContents(hover: Hover, expectedContents: string): void { + expect(hover.contents).toBeDefined(); + const contents = hover.contents as MarkupContent; + expect(contents.value).toContain(expectedContents); +} diff --git a/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts new file mode 100644 index 0000000..c7f0692 --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts @@ -0,0 +1,83 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { Diagnostic, DiagnosticSeverity, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { FAKE_WORKFLOW_DATA_PROVIDER } from "@gxwf/server-common/tests/testHelpers"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsValidationService } from "@gxwf/workflow-tests-language-service/src/services/validation"; +import "reflect-metadata"; +import { TYPES } from "../../src/types"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Validation Service", () => { + let service: WorkflowTestsValidationService; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + service = container.get(TYPES.WorkflowTestsValidationService); + }); + + async function validate( + contents: string, + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER + ): Promise { + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); + return await service.doValidation(documentContext); + } + + it("should warn about missing job and outputs properties", async () => { + const testDocumentContents = ` +- doc: The docs + `; + + const diagnostics = await validate(testDocumentContents); + + expect(diagnostics.length).toBe(2); + expect(diagnostics[0].message).toBe('Missing property "job".'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Warning); + expect(diagnostics[1].message).toBe('Missing property "outputs".'); + expect(diagnostics[1].severity).toBe(DiagnosticSeverity.Warning); + }); + + describe("Workflow Inputs/Outputs Validation", () => { + it("should pass validation when the inputs and outputs are defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + My fake dataset: data/input.txt + outputs: + My output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents); + + expect(diagnostics).not.toBeNull(); + }); + + it("should error when an input is not defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + Missing input: data/input.txt + outputs: + My output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].message).toBe('Input "Missing input" is not defined in the associated workflow.'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error); + }); + + it("should error when an output is not defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + My fake dataset: data/input.txt + outputs: + Missing output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].message).toBe('Output "Missing output" is not defined in the associated workflow.'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error); + }); + }); +}); diff --git a/server/packages/workflow-tests-language-service/tsconfig.json b/server/packages/workflow-tests-language-service/tsconfig.json new file mode 100644 index 0000000..fe8c62d --- /dev/null +++ b/server/packages/workflow-tests-language-service/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["ES2019", "WebWorker"], + "module": "commonjs", + "types": ["reflect-metadata"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "sourceMap": true, + "strict": true, + "composite": true, + "rootDirs": ["src", "tests"], + "rootDir": "../../../" + }, + "include": ["src/**/*", "../../../workflow-languages/schemas/*.json"], + "exclude": ["node_modules", ".vscode-test-web"] +} diff --git a/server/packages/yaml-language-service/package.json b/server/packages/yaml-language-service/package.json index 7a999a3..6c7bb09 100644 --- a/server/packages/yaml-language-service/package.json +++ b/server/packages/yaml-language-service/package.json @@ -6,6 +6,8 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", @@ -13,5 +15,8 @@ }, "scripts": { "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.8" } } diff --git a/server/packages/yaml-language-service/src/index.ts b/server/packages/yaml-language-service/src/index.ts index 82908d9..9d8fd89 100644 --- a/server/packages/yaml-language-service/src/index.ts +++ b/server/packages/yaml-language-service/src/index.ts @@ -1,4 +1,4 @@ import { YAMLDocument } from "./parser"; -import { LanguageService, getLanguageService } from "./yamlLanguageService"; +import { YAMLLanguageService, getLanguageService } from "./yamlLanguageService"; -export { YAMLDocument, LanguageService, getLanguageService }; +export { YAMLDocument, YAMLLanguageService, getLanguageService }; diff --git a/server/packages/yaml-language-service/src/inversify.config.ts b/server/packages/yaml-language-service/src/inversify.config.ts new file mode 100644 index 0000000..cef7a66 --- /dev/null +++ b/server/packages/yaml-language-service/src/inversify.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { YAMLLanguageService, getLanguageService } from "./yamlLanguageService"; + +export const TYPES = { + YAMLLanguageService: Symbol.for("YAMLLanguageService"), +}; + +export const YAMLLanguageServiceContainerModule = new ContainerModule((bind) => { + bind(TYPES.YAMLLanguageService).toConstantValue(getLanguageService()); +}); diff --git a/server/packages/yaml-language-service/src/parser/index.ts b/server/packages/yaml-language-service/src/parser/index.ts index 55749af..935df37 100644 --- a/server/packages/yaml-language-service/src/parser/index.ts +++ b/server/packages/yaml-language-service/src/parser/index.ts @@ -1,10 +1,9 @@ "use strict"; -import { Parser, Composer, Document, LineCounter, ParseOptions, DocumentOptions, SchemaOptions, Node } from "yaml"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { YAMLDocument, YAMLSubDocument } from "./yamlDocument"; +import { Composer, Document, DocumentOptions, LineCounter, ParseOptions, Parser, SchemaOptions } from "yaml"; import { TextBuffer } from "../utils/textBuffer"; -import { convertAST } from "./astConverter"; +import { YAMLDocument, YAMLSubDocument } from "./yamlDocument"; export { YAMLDocument }; @@ -42,6 +41,5 @@ export function parse(textDocument: TextDocument, parserOptions: ParserOptions = } function getParsedSubDocument(parsedDocument: Document, lineCounter: LineCounter): YAMLSubDocument { - const root = convertAST(undefined, parsedDocument.contents as Node, parsedDocument, lineCounter); - return new YAMLSubDocument(root, parsedDocument); + return new YAMLSubDocument(parsedDocument, lineCounter); } diff --git a/server/packages/yaml-language-service/src/parser/yamlDocument.ts b/server/packages/yaml-language-service/src/parser/yamlDocument.ts index af29a0e..14deeac 100644 --- a/server/packages/yaml-language-service/src/parser/yamlDocument.ts +++ b/server/packages/yaml-language-service/src/parser/yamlDocument.ts @@ -1,10 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Diagnostic, DiagnosticSeverity, Position } from "vscode-languageserver-types"; -import { Document, Node, YAMLError, YAMLWarning, visit } from "yaml"; +import { Document, LineCounter, Node, Pair, YAMLError, YAMLWarning, isNode, isPair, isScalar, visit } from "yaml"; +import { getIndentation, getParent } from "../utils"; import { guessIndentation } from "../utils/indentationGuesser"; import { TextBuffer } from "../utils/textBuffer"; -import { ASTNode, ObjectASTNodeImpl } from "./astTypes"; +import { convertAST } from "./astConverter"; +import { ASTNode, ObjectASTNodeImpl, YamlNode } from "./astTypes"; const FULL_LINE_ERROR = true; const YAML_SOURCE = "YAML"; @@ -42,6 +49,13 @@ export class YAMLDocument implements ParsedDocument { return this.subDocuments.at(0); } + /** Internal parsed document. + * Exposed for compatibility with reused code from RedHat's YAML Language Service. + */ + public get internalDocument(): YAMLSubDocument | undefined { + return this.mainDocument; + } + /** Returns basic YAML syntax errors or warnings. */ public get syntaxDiagnostics(): Diagnostic[] { if (!this._diagnostics) { @@ -136,11 +150,24 @@ export class YAMLDocument implements ParsedDocument { export class YAMLSubDocument { private _lineComments: LineComment[] | undefined; + private _root: ASTNode | undefined; + constructor( - public readonly root: ASTNode | undefined, - private readonly parsedDocument: Document + private readonly parsedDocument: Document, + private readonly _lineCounter: LineCounter ) {} + get root(): ASTNode | undefined { + if (!this._root) { + this.updateFromInternalDocument(); + } + return this._root; + } + + get internalDocument(): Document { + return this.parsedDocument; + } + get errors(): YAMLError[] { return this.parsedDocument.errors; } @@ -156,6 +183,10 @@ export class YAMLSubDocument { return this._lineComments; } + public updateFromInternalDocument(): void { + this._root = convertAST(undefined, this.parsedDocument.contents as Node, this.parsedDocument, this._lineCounter); + } + private collectLineComments(): LineComment[] { const lineComments = []; if (this.parsedDocument.commentBefore) { @@ -179,4 +210,151 @@ export class YAMLSubDocument { } return lineComments; } + + /** + * Create a deep copy of this document + */ + clone(): YAMLSubDocument { + const parsedDocumentCopy = this.parsedDocument.clone(); + const lineCounterCopy = new LineCounter(); + this._lineCounter.lineStarts.forEach((lineStart) => lineCounterCopy.addNewLine(lineStart)); + const copy = new YAMLSubDocument(parsedDocumentCopy, lineCounterCopy); + return copy; + } + + getNodeFromPosition( + positionOffset: number, + textBuffer: TextBuffer, + configuredIndentation?: number + ): [YamlNode | undefined, boolean] { + const position = textBuffer.getPosition(positionOffset); + const lineContent = textBuffer.getLineContent(position.line); + if (lineContent.trim().length === 0) { + return [this.findClosestNode(positionOffset, textBuffer, configuredIndentation), true]; + } + + const textAfterPosition = lineContent.substring(position.character); + const spacesAfterPositionMatch = textAfterPosition.match(/^([ ]+)\n?$/); + const areOnlySpacesAfterPosition = !!spacesAfterPositionMatch; + const countOfSpacesAfterPosition = spacesAfterPositionMatch?.[1].length ?? 0; + let closestNode: Node | undefined = undefined; + visit(this.parsedDocument, (_, node) => { + if (!node) { + return; + } + const range = (node as Node).range; + if (!range) { + return; + } + + const isNullNodeOnTheLine = (): boolean => + areOnlySpacesAfterPosition && + positionOffset + countOfSpacesAfterPosition === range[2] && + isScalar(node) && + node.value === null; + + if ((range[0] <= positionOffset && range[1] >= positionOffset) || isNullNodeOnTheLine()) { + closestNode = node as Node; + } else { + return visit.SKIP; + } + }); + + return [closestNode, false]; + } + + findClosestNode(offset: number, textBuffer: TextBuffer, configuredIndentation?: number): YamlNode | undefined { + let offsetDiff = this.parsedDocument.range?.[2] ?? 0; + let maxOffset = this.parsedDocument.range?.[0] ?? 0; + let closestNode: YamlNode | undefined = undefined; + visit(this.parsedDocument, (key, node) => { + if (!node) { + return; + } + const range = (node as Node).range; + if (!range) { + return; + } + const diff = range[1] - offset; + if (maxOffset <= range[0] && diff <= 0 && Math.abs(diff) <= offsetDiff) { + offsetDiff = Math.abs(diff); + maxOffset = range[0]; + closestNode = node as Node; + } + }); + + const position = textBuffer.getPosition(offset); + const lineContent = textBuffer.getLineContent(position.line); + const indentation = getIndentation(lineContent, position.character); + + if (isScalar(closestNode) && (closestNode as Pair).value === null) { + return closestNode; + } + + if (indentation === position.character) { + closestNode = this.getProperParentByIndentation(indentation, closestNode, textBuffer, "", configuredIndentation); + } + + return closestNode; + } + + private getProperParentByIndentation( + indentation: number, + node: YamlNode | undefined, + textBuffer: TextBuffer, + currentLine: string, + configuredIndentation?: number, + rootParent?: YamlNode + ): YamlNode { + if (!node) { + return this.parsedDocument.contents as Node; + } + configuredIndentation = !configuredIndentation ? 2 : configuredIndentation; + if (isNode(node) && node.range) { + const position = textBuffer.getPosition(node.range[0]); + const lineContent = textBuffer.getLineContent(position.line); + currentLine = currentLine === "" ? lineContent.trim() : currentLine; + if (currentLine.startsWith("-") && indentation === configuredIndentation && currentLine === lineContent.trim()) { + position.character += indentation; + } + if (position.character > indentation && position.character > 0) { + const parent = this.getParent(node); + if (parent) { + return this.getProperParentByIndentation( + indentation, + parent, + textBuffer, + currentLine, + configuredIndentation, + rootParent + ); + } + } else if (position.character < indentation) { + const parent = this.getParent(node); + if (isPair(parent) && isNode(parent.value)) { + return parent.value; + } else if (isPair(rootParent) && isNode(rootParent.value)) { + return rootParent.value; + } + } else { + return node; + } + } else if (isPair(node)) { + rootParent = node; + const parent = this.getParent(node); + return this.getProperParentByIndentation( + indentation, + parent, + textBuffer, + currentLine, + configuredIndentation, + rootParent + ); + } + return node; + } + + getParent(node: YamlNode): YamlNode | undefined { + return getParent(this.parsedDocument, node); + } } diff --git a/server/packages/yaml-language-service/src/utils/index.ts b/server/packages/yaml-language-service/src/utils/index.ts new file mode 100644 index 0000000..d681b4a --- /dev/null +++ b/server/packages/yaml-language-service/src/utils/index.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Document, Node, Range, YAMLMap, YAMLSeq, isDocument, isScalar, visit } from "yaml"; +import { YamlNode } from "../parser/astTypes"; +import { CharCode } from "../parser/charCode"; + +export function getIndentation(lineContent: string, lineOffset: number): number { + if (lineContent.length < lineOffset) { + return 0; + } + + for (let i = 0; i < lineOffset; i++) { + const char = lineContent.charCodeAt(i); + if (char !== CharCode.Space && char !== CharCode.Tab) { + return i; + } + } + + // assuming that current position is indentation + return lineOffset; +} + +export function getParent(doc: Document, nodeToFind: YamlNode): YamlNode | undefined { + let parentNode: Node | undefined = undefined; + visit(doc, (_, node, path) => { + if (node === nodeToFind || rangeMatches(node as HasRange, nodeToFind as HasRange)) { + parentNode = path[path.length - 1] as Node; + return visit.BREAK; + } + }); + + if (isDocument(parentNode)) { + return undefined; + } + + return parentNode; +} + +export function indexOf(seq: YAMLSeq, item: YamlNode): number | undefined { + for (const [i, obj] of seq.items.entries()) { + if (item === obj) { + return i; + } + } + return undefined; +} + +export function isMapContainsEmptyPair(map: YAMLMap): boolean { + if (map.items.length > 1) { + return false; + } + + const pair = map.items[0]; + return isScalar(pair.key) && isScalar(pair.value) && pair.key.value === "" && !pair.value.value; +} + +export interface HasRange { + range: Range; +} + +export function rangeMatches(nodeA: HasRange, nodeB: HasRange): boolean { + if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { + return nodeA.range.every((value, index) => value === nodeB.range[index]); + } + return false; +} diff --git a/server/packages/yaml-language-service/src/utils/textBuffer.ts b/server/packages/yaml-language-service/src/utils/textBuffer.ts index 5b0f9d5..1977f0d 100644 --- a/server/packages/yaml-language-service/src/utils/textBuffer.ts +++ b/server/packages/yaml-language-service/src/utils/textBuffer.ts @@ -5,6 +5,7 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import { Position, Range } from "vscode-languageserver-types"; +import { getIndentation } from "."; import { CharCode } from "../parser/charCode"; interface FullTextDocument { @@ -83,7 +84,7 @@ export class TextBuffer { public getLineIndentationAtOffset(offset: number): number { const position = this.getPosition(offset); const lineContent = this.getLineContent(position.line); - const indentation = this.getIndentation(lineContent, position.character); + const indentation = getIndentation(lineContent, position.character); return indentation; } @@ -103,20 +104,4 @@ export class TextBuffer { } return currentLine; } - - private getIndentation(lineContent: string, lineOffset: number): number { - if (lineContent.length < lineOffset) { - return 0; - } - - for (let i = 0; i < lineOffset; i++) { - const char = lineContent.charCodeAt(i); - if (char !== CharCode.Space && char !== CharCode.Tab) { - return i; - } - } - - // assuming that current position is indentation - return lineOffset; - } } diff --git a/server/packages/yaml-language-service/src/yamlLanguageService.ts b/server/packages/yaml-language-service/src/yamlLanguageService.ts index 1a23010..f87d7c9 100644 --- a/server/packages/yaml-language-service/src/yamlLanguageService.ts +++ b/server/packages/yaml-language-service/src/yamlLanguageService.ts @@ -15,14 +15,14 @@ export interface CustomFormatterOptions { lineWidth?: number; } -export interface LanguageService { +export interface YAMLLanguageService { configure(settings: LanguageSettings): void; parseYAMLDocument(document: TextDocument): YAMLDocument; doValidation(yamlDocument: YAMLDocument): Promise; doFormat(document: TextDocument, options: FormattingOptions & CustomFormatterOptions): TextEdit[]; } -export function getLanguageService(): LanguageService { +export function getLanguageService(): YAMLLanguageService { const formatter = new YAMLFormatter(); const validator = new YAMLValidation(); return { diff --git a/server/tsconfig.json b/server/tsconfig.json index 5200861..33489a2 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,19 +3,19 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, "sourceMap": true, "strict": true, "composite": true, + "baseUrl": ".", "paths": { "@schemas/*": ["../workflow-languages/schemas/*"] } }, - "files": [], - "include": [], - "exclude": [], "references": [ { "path": "packages/server-common" diff --git a/shared.webpack.config.js b/shared.webpack.config.js index 8bff832..e5d5c07 100644 --- a/shared.webpack.config.js +++ b/shared.webpack.config.js @@ -58,7 +58,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/ extConfig) { }, output: { filename: "[name].js", - path: path.join(extConfig.context, "dist"), + path: path.join(extConfig.context ?? "", "dist"), libraryTarget: "commonjs", }, devtool: "source-map", diff --git a/shared/src/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts new file mode 100644 index 0000000..41ff0be --- /dev/null +++ b/shared/src/requestsDefinitions.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +export namespace LSRequestIdentifiers { + export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; + export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; + export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; + export const GET_WORKFLOW_OUTPUTS = "galaxy-workflows-ls.getWorkflowOutputs"; +} + +export interface CleanWorkflowDocumentParams { + uri: string; +} + +export interface CleanWorkflowDocumentResult { + error: string; +} + +export interface CleanWorkflowContentsParams { + contents: string; +} + +export interface CleanWorkflowContentsResult { + contents: string; +} + +export interface TargetWorkflowDocumentParams { + /** The URI of the target workflow document. */ + uri: string; +} + +export type WorkflowDataType = + | "color" //TODO: this type seems to be missing in format2 schema + | "null" + | "boolean" + | "int" + | "long" + | "float" + | "double" + | "string" + | "integer" + | "text" + | "File" + | "data" + | "collection"; + +export interface WorkflowInput { + name: string; + type: WorkflowDataType; + doc: string; +} + +export interface GetWorkflowInputsResult { + inputs: WorkflowInput[]; +} + +//TODO: unify format1 and format2 output definitions +export interface WorkflowOutput { + name: string; + uuid?: string; + doc?: string; + type?: WorkflowDataType; +} + +export interface GetWorkflowOutputsResult { + outputs: WorkflowOutput[]; +} diff --git a/test-data/json/validation/test_wf_05.ga b/test-data/json/validation/test_wf_05.ga new file mode 100644 index 0000000..3bf04dd --- /dev/null +++ b/test-data/json/validation/test_wf_05.ga @@ -0,0 +1,201 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "", + "comments": [], + "format-version": "0.1", + "name": "Workflow with inputs only", + "steps": { + "0": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Dataset Input" + } + ], + "label": "Dataset Input", + "name": "Input dataset", + "outputs": [], + "position": { + "left": 21, + "top": 0 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null}", + "tool_version": null, + "type": "data_input", + "uuid": "f38a0899-4d6a-42ef-a25e-fb860d624230", + "when": null, + "workflow_outputs": [] + }, + "1": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 1, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Collection Input" + } + ], + "label": "Collection Input", + "name": "Input dataset collection", + "outputs": [], + "position": { + "left": 20, + "top": 123 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null, \"collection_type\": \"list\"}", + "tool_version": null, + "type": "data_collection_input", + "uuid": "ee32048f-8e39-49cd-a2ff-cd24582a6bac", + "when": null, + "workflow_outputs": [] + }, + "2": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 2, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Text Param" + } + ], + "label": "Text Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 20, + "top": 260 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"text\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "f15e91fb-b5f1-4a3d-910a-436e617afc53", + "when": null, + "workflow_outputs": [] + }, + "3": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 3, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Integer Param" + } + ], + "label": "Integer Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 24, + "top": 374 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"integer\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "847d6938-c7aa-41fa-a139-611f4aaa900d", + "when": null, + "workflow_outputs": [] + }, + "4": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 4, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Float Param" + } + ], + "label": "Float Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 22, + "top": 489 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"float\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "dd9fdf0e-76af-4de8-a1d8-e6a2f6265013", + "when": null, + "workflow_outputs": [] + }, + "5": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 5, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Boolean Param" + } + ], + "label": "Boolean Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 12, + "top": 596 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"boolean\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "e600a8b4-3255-4f22-9452-71bde2b2a687", + "when": null, + "workflow_outputs": [] + }, + "6": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 6, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Color Param" + } + ], + "label": "Color Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 0, + "top": 712 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"color\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "28d79f2e-a8d0-49e1-aab3-a032b2d3d327", + "when": null, + "workflow_outputs": [] + } + }, + "tags": [], + "uuid": "a1d51848-2a7b-4ec1-b1a7-624e25d9c47a", + "version": 1 +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 729c0c4..0d25d73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/workflow-languages/schemas/tests.schema.json b/workflow-languages/schemas/tests.schema.json new file mode 100644 index 0000000..26fa993 --- /dev/null +++ b/workflow-languages/schemas/tests.schema.json @@ -0,0 +1,4187 @@ +{ + "$defs": { + "AssertAttributeIs": { + "additionalProperties": false, + "description": "Asserts the XML ``attribute`` for the element (or tag) with the specified\nXPath-like ``path`` is the specified ``text``, e.g. ```xml <attribute_is\npath=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" /> ``` The\nassertion implicitly also asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the equality) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "text": { + "description": "Text to check for.", + "title": "Text", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path", "text"], + "title": "AssertAttributeIs", + "type": "object" + }, + "AssertAttributeMatches": { + "additionalProperties": false, + "description": "Asserts the XML ``attribute`` for the element (or tag) with the specified\nXPath-like ``path`` matches the regular expression specified by ``expression``,\ne.g. ```xml <attribute_matches path=\"outerElement/innerElement2\"\nattribute=\"foo2\" expression=\"bar\\d+\" /> ``` The assertion implicitly also\nasserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the matching) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "expression": { + "description": "The regular expression to use.", + "title": "Expression", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path", "expression"], + "title": "AssertAttributeMatches", + "type": "object" + }, + "AssertElementText": { + "additionalProperties": false, + "description": "This tag allows the developer to recurisively specify additional assertions\nas child elements about just the text contained in the element specified by the\nXPath-like ``path``, e.g. ```xml <element_text\npath=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" /> </element_text> ``` The\nassertion implicitly also asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the implicit assertions can be inverted.\nThe sub-assertions, which have their own ``negate`` attribute, are not affected\nby ``negate``.\n$attribute_list::5", + "properties": { + "has_text": { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "title": "Has Text", + "type": "array" + }, + "not_has_text": { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "title": "Not Has Text", + "type": "array" + }, + "has_text_matching": { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "title": "Has Text Matching", + "type": "array" + }, + "has_line": { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "title": "Has Line", + "type": "array" + }, + "has_line_matching": { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "title": "Has Line Matching", + "type": "array" + }, + "has_n_lines": { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "title": "Has N Lines", + "type": "array" + }, + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path"], + "title": "AssertElementText", + "type": "object" + }, + "AssertElementTextIs": { + "additionalProperties": false, + "description": "Asserts the text of the XML element with the specified XPath-like ``path``\nis the specified ``text``, e.g. ```xml <element_text_is\npath=\"BlastOutput_program\" text=\"blastp\" /> ``` The assertion implicitly\nalso asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the equality) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "text": { + "description": "Text to check for.", + "title": "Text", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path", "text"], + "title": "AssertElementTextIs", + "type": "object" + }, + "AssertElementTextMatches": { + "additionalProperties": false, + "description": "Asserts the text of the XML element with the specified XPath-like ``path``\nmatches the regular expression defined by ``expression``, e.g. ```xml\n<element_text_matches path=\"BlastOutput_version\"\nexpression=\"BLASTP\\s+2\\.2.*\"/> ``` The assertion implicitly also asserts\nthat an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the matching) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "expression": { + "description": "The regular expression to use.", + "title": "Expression", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path", "expression"], + "title": "AssertElementTextMatches", + "type": "object" + }, + "AssertHasArchiveMember": { + "additionalProperties": false, + "description": "This tag allows to check if ``path`` is contained in a compressed file. The\npath is a regular expression that is matched against the full paths of the\nobjects in the compressed file (remember that \"matching\" means it is checked if\na prefix of the full path of an archive member is described by the regular\nexpression). Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``.\nNote that.\n\ndepending on the archive creation method:\n- full paths of the members may be prefixed with ``./``\n- directories may be treated as empty files\n```xml\n<has_archive_member path=\"./path/to/my-file.txt\"/>\n```\nWith ``n`` and ``delta`` (or ``min`` and ``max``) assertions on the number of\narchive members matching ``path`` can be expressed. The following could be used,\ne.g., to assert an archive containing n&plusmn;1 elements out of which at least\n4 need to have a ``txt`` extension.\n```xml\n<has_archive_member path=\".*\" n=\"10\" delta=\"1\"/>\n<has_archive_member path=\".*\\.txt\" min=\"4\"/>\n```\nIn addition the tag can contain additional assertions as child elements about\nthe first member in the archive matching the regular expression ``path``. For\ninstance\n```xml\n<has_archive_member path=\".*/my-file.txt\">\n<not_has_text text=\"EDK72998.1\"/>\n</has_archive_member>\n```\nIf the ``all`` attribute is set to ``true`` then all archive members are subject\nto the assertions. Note that, archive members matching the ``path`` are sorted\nalphabetically.\nThe ``negate`` attribute of the ``has_archive_member`` assertion only affects\nthe asserts on the presence and number of matching archive members, but not any\nsub-assertions (which can offer the ``negate`` attribute on their own). The\ncheck if the file is an archive at all, which is also done by the function, is\nnot affected.\n$attribute_list::5", + "properties": { + "has_size": { + "items": { + "$ref": "#/$defs/AssertHasSize" + }, + "title": "Has Size", + "type": "array" + }, + "has_text": { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "title": "Has Text", + "type": "array" + }, + "not_has_text": { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "title": "Not Has Text", + "type": "array" + }, + "has_text_matching": { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "title": "Has Text Matching", + "type": "array" + }, + "has_line": { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "title": "Has Line", + "type": "array" + }, + "has_line_matching": { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "title": "Has Line Matching", + "type": "array" + }, + "has_n_lines": { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "title": "Has N Lines", + "type": "array" + }, + "has_n_columns": { + "items": { + "$ref": "#/$defs/AssertHasNcolumns" + }, + "title": "Has N Columns", + "type": "array" + }, + "has_json_property_with_value": { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + "title": "Has Json Property With Value", + "type": "array" + }, + "has_json_property_with_text": { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + "title": "Has Json Property With Text", + "type": "array" + }, + "is_valid_xml": { + "items": { + "$ref": "#/$defs/AssertIsValidXml" + }, + "title": "Is Valid Xml", + "type": "array" + }, + "xml_element": { + "items": { + "$ref": "#/$defs/AssertXmlelement" + }, + "title": "Xml Element", + "type": "array" + }, + "has_element_with_path": { + "items": { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + "title": "Has Element With Path", + "type": "array" + }, + "has_n_elements_with_path": { + "items": { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + "title": "Has N Elements With Path", + "type": "array" + }, + "element_text_matches": { + "items": { + "$ref": "#/$defs/AssertElementTextMatches" + }, + "title": "Element Text Matches", + "type": "array" + }, + "element_text_is": { + "items": { + "$ref": "#/$defs/AssertElementTextIs" + }, + "title": "Element Text Is", + "type": "array" + }, + "attribute_matches": { + "items": { + "$ref": "#/$defs/AssertAttributeMatches" + }, + "title": "Attribute Matches", + "type": "array" + }, + "attribute_is": { + "items": { + "$ref": "#/$defs/AssertAttributeIs" + }, + "title": "Attribute Is", + "type": "array" + }, + "element_text": { + "items": { + "$ref": "#/$defs/AssertElementText" + }, + "title": "Element Text", + "type": "array" + }, + "has_h5_keys": { + "items": { + "$ref": "#/$defs/AssertHasH5Keys" + }, + "title": "Has H5 Keys", + "type": "array" + }, + "has_h5_attribute": { + "items": { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + "title": "Has H5 Attribute", + "type": "array" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The regular expression specifying the archive member.", + "title": "Path" + }, + "all": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first", + "title": "All" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + } + }, + "title": "AssertHasArchiveMember", + "type": "object" + }, + "AssertHasElementWithPath": { + "additionalProperties": false, + "description": "Asserts the XML output contains at least one element (or tag) with the\nspecified XPath-like ``path``, e.g. ```xml <has_element_with_path\npath=\"BlastOutput_param/Parameters/Parameters_matrix\" /> ``` With ``negate``\nthe result of the assertion can be inverted.\n\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path"], + "title": "AssertHasElementWithPath", + "type": "object" + }, + "AssertHasH5Attribute": { + "additionalProperties": false, + "description": "Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.\n```xml\n<has_h5_attribute key=\"nchroms\" value=\"15\" />\n```\n$attribute_list::5", + "properties": { + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "HDF5 attribute to check value of.", + "title": "Key" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expected value of HDF5 attribute to check.", + "title": "Value" + } + }, + "title": "AssertHasH5Attribute", + "type": "object" + }, + "AssertHasH5Keys": { + "additionalProperties": false, + "description": "Asserts HDF5 output has a set of attributes (``keys``), specified as a\ncomma-separated list, e.g.\n```xml\n<has_h5_keys keys=\"bins,chroms,indexes,pixels,chroms/lengths\" />\n```\n$attribute_list::5", + "properties": { + "keys": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Comma-separated list of HDF5 attributes to check for.", + "title": "Keys" + } + }, + "title": "AssertHasH5Keys", + "type": "object" + }, + "AssertHasJsonPropertyWithText": { + "additionalProperties": false, + "description": "Asserts the JSON document contains a property or key with the specified text\n(i.e. string) value.\n\n```xml\n<has_json_property_with_text property=\"color\" text=\"red\" />\n```\n$attribute_list::5", + "properties": { + "property": { + "description": "JSON property to search the target for.", + "title": "Property", + "type": "string" + }, + "text": { + "description": "Text value to search for.", + "title": "Text", + "type": "string" + } + }, + "required": ["property", "text"], + "title": "AssertHasJsonPropertyWithText", + "type": "object" + }, + "AssertHasJsonPropertyWithValue": { + "additionalProperties": false, + "description": "Asserts the JSON document contains a property or key with the specified JSON\nvalue.\n\n```xml\n<has_json_property_with_value property=\"skipped_columns\" value=\"[1, 3, 5]\" />\n```\n$attribute_list::5", + "properties": { + "property": { + "description": "JSON property to search the target for.", + "title": "Property", + "type": "string" + }, + "value": { + "description": "JSON-ified value to search for. This will be converted from an XML string to JSON with Python's json.loads function.", + "title": "Value", + "type": "string" + } + }, + "required": ["property", "value"], + "title": "AssertHasJsonPropertyWithValue", + "type": "object" + }, + "AssertHasLine": { + "additionalProperties": false, + "description": "Asserts a line matching the specified string (``line``) appears in the\noutput (e.g. ``<has_line line=\"A full example line.\" />``).\n\nIf the ``line`` is expected\nto occur a particular number of times, this value can be specified using ``n``.\nOptionally also with a certain ``delta``. Alternatively the range of expected\noccurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", + "properties": { + "line": { + "description": "The line to check for", + "title": "Line", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["line"], + "title": "AssertHasLine", + "type": "object" + }, + "AssertHasLineMatching": { + "additionalProperties": false, + "description": "Asserts a line matching the specified regular expression (``expression``)\nappears in the output (e.g. ``<has_line_matching\nexpression=\".*\\s+127489808\\s+127494553\" />``).\n\nIf a particular number of matching lines is expected, this value can be\nspecified using ``n``. Optionally also with ``delta``. Alternatively the range\nof expected occurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", + "properties": { + "expression": { + "description": "Regular expression to check for", + "title": "Expression", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["expression"], + "title": "AssertHasLineMatching", + "type": "object" + }, + "AssertHasNcolumns": { + "additionalProperties": false, + "description": "Asserts tabular output (actually only the first line) contains the specified\nnumber (``n``) of columns (e.g. ``<has_n_columns n=\"3\"/>``) optionally\nalso with ``delta``.\n\nAlternatively the range of expected occurences can be specified by\n``min`` and/or ``max``. Optionally a column separator (``sep``, default is\n``\\t``) `and comment character(s) can be specified (``comment``, default is\nempty string), then the first non-comment line is used for determining the\nnumber of columns.\n$attribute_list::5", + "properties": { + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + }, + "sep": { + "default": " ", + "description": "Separator defining columns, default: tab", + "title": "Sep", + "type": "string" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Comment character(s) used to skip comment lines (which should not be used for counting columns)", + "title": "Comment" + } + }, + "title": "AssertHasNcolumns", + "type": "object" + }, + "AssertHasNelementsWithPath": { + "additionalProperties": false, + "description": "Asserts the XML output contains the specified number (``n``, optionally with\n``delta``) of elements (or tags) with the specified XPath-like ``path``, e.g.\n```xml <has_n_elements_with_path n=\"9\"\npath=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" /> ```\nAlternatively to ``n`` and ``delta`` also the ``min`` and ``max`` attributes\ncan be used to specify the range of the expected number of occurences.\n\nWith ``negate`` the result of the assertion can be inverted.\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path"], + "title": "AssertHasNelementsWithPath", + "type": "object" + }, + "AssertHasNlines": { + "additionalProperties": false, + "description": "Asserts that an output contains ``n`` lines, allowing for a difference of\n``delta`` (default is 0), e.g. ``<has_n_lines n=\"3\" delta=\"1\"/>``.\n\nAlternatively the range of expected occurences can be specified by ``min``\nand/or ``max``.\n$attribute_list::5", + "properties": { + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "title": "AssertHasNlines", + "type": "object" + }, + "AssertHasSize": { + "additionalProperties": false, + "description": "Asserts the output has a specific size (in bytes) of ``value`` plus minus\n``delta``, e.g. ``<has_size value=\"10000\" delta=\"100\" />``.\n\nAlternatively the range of the expected size can be specified by ``min`` and/or\n``max``.\n$attribute_list::5", + "properties": { + "value": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired size of the output (in bytes), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Value" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum allowed size difference (default is 0). The observed size has to be in the range ``value +- delta``. Can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "title": "AssertHasSize", + "type": "object" + }, + "AssertHasText": { + "additionalProperties": false, + "description": "Asserts the specified ``text`` appears in the output (e.g. ``<has_text\ntext=\"chr7\">``).\n\nIf the ``text`` is expected to occur a particular number of\ntimes, this value can be specified using ``n``. Optionally also with a certain\n``delta``. Alternatively the range of expected occurences can be specified by\n``min`` and/or ``max``.\n$attribute_list::5", + "properties": { + "text": { + "description": "Text to check for", + "title": "Text", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["text"], + "title": "AssertHasText", + "type": "object" + }, + "AssertHasTextMatching": { + "additionalProperties": false, + "description": "Asserts text matching the specified regular expression (``expression``)\nappears in the output (e.g. ``<has_text_matching expression=\"1274\\d+53\"\n/>`` ).\n\nIf the\nregular expression is expected to match a particular number of times, this value\ncan be specified using ``n``. Note only non-overlapping occurences are counted.\nOptionally also with a certain ``delta``. Alternatively the range of expected\noccurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", + "properties": { + "expression": { + "description": "Regular expression to check for", + "title": "Expression", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["expression"], + "title": "AssertHasTextMatching", + "type": "object" + }, + "AssertIsValidXml": { + "additionalProperties": false, + "description": "Asserts the output is a valid XML file (e.g. ``<is_valid_xml />``).\n\n$attribute_list::5", + "properties": {}, + "title": "AssertIsValidXml", + "type": "object" + }, + "AssertNotHasText": { + "additionalProperties": false, + "description": "Asserts the specified ``text`` does not appear in the output (e.g.\n``<not_has_text text=\"chr8\" />``).\n\n$attribute_list::5", + "properties": { + "text": { + "description": "Text to check for", + "title": "Text", + "type": "string" + } + }, + "required": ["text"], + "title": "AssertNotHasText", + "type": "object" + }, + "AssertXmlelement": { + "additionalProperties": false, + "description": "Assert if the XML file contains element(s) or tag(s) with the specified\n[XPath-like ``path``](https://lxml.de/xpathxslt.html). If ``n`` and ``delta``\nor ``min`` and ``max`` are given also the number of occurences is checked.\n```xml\n<assert_contents>\n<xml_element path=\"./elem\"/>\n<xml_element path=\"./elem/more[2]\"/>\n<xml_element path=\".//more\" n=\"3\" delta=\"1\"/>\n</assert_contents>\n```\nWith ``negate=\"true\"`` the outcome of the assertions wrt the precence and number\nof ``path`` can be negated. If there are any sub assertions then check them against\n- the content of the attribute ``attribute``\n- the element's text if no attribute is given\n```xml\n<assert_contents>\n<xml_element path=\"./elem/more[2]\" attribute=\"name\">\n<has_text_matching expression=\"foo$\"/>\n</xml_element>\n</assert_contents>\n```\nSub-assertions are not subject to the ``negate`` attribute of ``xml_element``.\nIf ``all`` is ``true`` then the sub assertions are checked for all occurences.\nNote that all other XML assertions can be expressed by this assertion (Galaxy\nalso implements the other assertions by calling this one).\n$attribute_list::5", + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "all": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first", + "title": "All" + }, + "attribute": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The name of the attribute to apply sub-assertion on. If not given then the element text is used", + "title": "Attribute" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion.", + "title": "Negate" + } + }, + "required": ["path"], + "title": "AssertXmlelement", + "type": "object" + }, + "Collection": { + "properties": { + "class": { + "const": "Collection", + "title": "Class" + }, + "collection_type": { + "default": "list", + "title": "Collection Type", + "type": "string" + }, + "elements": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/CollectionElement" + }, + { + "$ref": "#/$defs/LocationFileElement" + }, + { + "$ref": "#/$defs/PathFileElement" + }, + { + "$ref": "#/$defs/CompositeDataFileElement" + } + ] + }, + "title": "Elements", + "type": "array" + } + }, + "required": ["class", "elements"], + "title": "Collection", + "type": "object" + }, + "CollectionElement": { + "properties": { + "class": { + "const": "Collection", + "title": "Class" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "elements": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/CollectionElement" + }, + { + "$ref": "#/$defs/LocationFileElement" + }, + { + "$ref": "#/$defs/PathFileElement" + }, + { + "$ref": "#/$defs/CompositeDataFileElement" + } + ] + }, + "title": "Elements", + "type": "array" + }, + "type": { + "default": "list", + "title": "Type", + "type": "string" + } + }, + "required": ["class", "identifier", "elements"], + "title": "CollectionElement", + "type": "object" + }, + "CompositeDataFile": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "composite_data": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Composite Data", + "type": "array" + } + }, + "required": ["class", "composite_data"], + "title": "CompositeDataFile", + "type": "object" + }, + "CompositeDataFileElement": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "composite_data": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Composite Data", + "type": "array" + } + }, + "required": ["class", "identifier", "composite_data"], + "title": "CompositeDataFileElement", + "type": "object" + }, + "Job": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/Collection" + }, + { + "$ref": "#/$defs/LocationFile" + }, + { + "$ref": "#/$defs/PathFile" + }, + { + "$ref": "#/$defs/CompositeDataFile" + }, + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/Collection" + }, + { + "$ref": "#/$defs/LocationFile" + }, + { + "$ref": "#/$defs/PathFile" + }, + { + "$ref": "#/$defs/CompositeDataFile" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Job" + }, + "LocationFile": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "location": { + "title": "Location", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + } + }, + "required": ["class", "location"], + "title": "LocationFile", + "type": "object" + }, + "LocationFileElement": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "location": { + "title": "Location", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + } + }, + "required": ["class", "identifier", "location"], + "title": "LocationFileElement", + "type": "object" + }, + "PathFile": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "path": { + "title": "Path", + "type": "string" + } + }, + "required": ["class", "path"], + "title": "PathFile", + "type": "object" + }, + "PathFileElement": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + } + }, + "required": ["class", "identifier", "path"], + "title": "PathFileElement", + "type": "object" + }, + "PermissiveBooleanValue": { + "enum": ["0", "1", "true", "false", "True", "False", "yes", "no"], + "title": "PermissiveBooleanValue", + "type": "string" + }, + "Test": { + "additionalProperties": false, + "properties": { + "doc": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Describes the purpose of the test.", + "title": "Doc" + }, + "job": { + "allOf": [ + { + "$ref": "#/$defs/Job" + } + ], + "description": "Defines job to execute. Can be a path to a file or an line dictionary describing the job inputs." + }, + "outputs": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "$ref": "#/$defs/TestOutputCollection" + }, + { + "$ref": "#/$defs/TestOutputCollectionDeprecated" + }, + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Defines assertions about outputs (datasets, collections or parameters). Each key corresponds to a labeled output, values are dictionaries describing the expected output.", + "title": "Outputs", + "type": "object" + } + }, + "required": ["job", "outputs"], + "title": "Test", + "type": "object" + }, + "TestAssertions": { + "additionalProperties": false, + "description": "This tag set defines a sequence of checks or assertions to run against the\ntarget output.\n\nThis tag requires no attributes, but child tags should be used to\ndefine the assertions to make about the output. The functional test framework\nmakes it easy to extend Galaxy with such tags, the following table summarizes\nmany of the default assertion tags that come with Galaxy and examples of each\ncan be found below.\nThe implementation of these tags are simply Python functions defined in the\n[/lib/galaxy/tool_util/verify/asserts](https://github.com/galaxyproject/galaxy/tree/dev/lib/galaxy/tool_util/verify/asserts)\nmodule.", + "properties": { + "has_size": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasSize" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasSize" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Size" + }, + "has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Text" + }, + "not_has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertNotHasText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Not Has Text" + }, + "has_text_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasTextMatching" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Text Matching" + }, + "has_line": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLine" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Line" + }, + "has_line_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLineMatching" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Line Matching" + }, + "has_n_lines": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNlines" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Lines" + }, + "has_n_columns": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNcolumns" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNcolumns" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Columns" + }, + "has_archive_member": { + "items": { + "$ref": "#/$defs/AssertHasArchiveMember" + }, + "title": "Has Archive Member", + "type": "array" + }, + "is_valid_xml": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertIsValidXml" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertIsValidXml" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Is Valid Xml" + }, + "xml_element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertXmlelement" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertXmlelement" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Xml Element" + }, + "has_element_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Element With Path" + }, + "has_n_elements_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Elements With Path" + }, + "element_text_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextMatches" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text Matches" + }, + "element_text_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextIs" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text Is" + }, + "attribute_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeMatches" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attribute Matches" + }, + "attribute_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeIs" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attribute Is" + }, + "element_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text" + }, + "has_json_property_with_value": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Json Property With Value" + }, + "has_json_property_with_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Json Property With Text" + }, + "has_h5_keys": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Keys" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Keys" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has H5 Keys" + }, + "has_h5_attribute": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has H5 Attribute" + } + }, + "title": "TestAssertions", + "type": "object" + }, + "TestDiscoveredDataset": { + "additionalProperties": false, + "description": "This directive specifies a test for an output's discovered dataset.\n\nIt acts as an\n``output`` test tag in many ways and can define any tests of that tag (e.g.\n``assert_contents``, ``value``, ``compare``, ``md5``, ``checksum``, ``metadata``, etc...).\n### Example\nThe functional test tool\n[multi_output_assign_primary.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/multi_output_assign_primary.xml)\nprovides a demonstration of using this tag.\n```xml\n<outputs>\n<data format=\"tabular\" name=\"sample\">\n<discover_datasets pattern=\"(?P&lt;designation&gt;.+)\\.report\\.tsv\" ext=\"tabular\" visible=\"true\" assign_primary_output=\"true\" />\n</data>\n</outputs>\n<test>\n<param name=\"num_param\" value=\"7\" />\n<param name=\"input\" ftype=\"txt\" value=\"simple_line.txt\"/>\n<output name=\"sample\">\n<assert_contents>\n<has_line line=\"1\" />\n</assert_contents>\n<!-- no sample1 it was consumed by named output \"sample\" -->\n<discovered_dataset designation=\"sample2\" ftype=\"tabular\">\n<assert_contents><has_line line=\"2\" /></assert_contents>\n</discovered_dataset>\n<discovered_dataset designation=\"sample3\" ftype=\"tabular\">\n<assert_contents><has_line line=\"3\" /></assert_contents>\n</discovered_dataset>\n</output>\n</test>\n```\nNote that this tool uses ``assign_primary_output=\"true\"`` for ``<discover_datasets>``. Hence, the content of the first discovered dataset (which is the first in the alphabetically sorted list of discovered designations) is checked directly in the ``<output>`` tag of the test.", + "properties": { + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" + } + ], + "default": null, + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + }, + "designation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The designation of the discovered dataset.", + "title": "Designation" + } + }, + "title": "TestDiscoveredDataset", + "type": "object" + }, + "TestExtraFile": { + "additionalProperties": false, + "description": "Define test for extra files on corresponding output.", + "properties": { + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" + } + ], + "default": null, + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + }, + "type_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extra file type (either ``file`` or ``directory``).", + "title": "Type Value" + } + }, + "title": "TestExtraFile", + "type": "object" + }, + "TestOutput": { + "additionalProperties": false, + "description": "This tag set defines the variable that names the output dataset for the\nfunctional test framework. The functional test framework will execute the tool\nusing the parameters defined in the ``<param>`` tag sets and generate a\ntemporary file, which will either be compared with the file named in the\n``file`` attribute value or checked against assertions made by a child\n``assert_contents`` tag to verify that the tool is functionally correct.\nDifferent methods can be chosen for the comparison with the local file\nspecified.\n\nby ``file`` using the ``compare`` attribute:\n- ``diff``: uses diff to compare the history data set and the file provided by\n``file``. Compressed files are decompressed before the compariopm if\n``decompress`` is set to ``true``. BAM files are converted to SAM before the\ncomparision and for pdf some special rules are implemented. The number of\nallowed differences can be set with ``lines_diff``. If ``sort=\"true\"`` history\nand local data is sorted before the comparison.\n- ``re_match``: each line of the history data set is compared to the regular\nexpression specified in the corresponding line of the ``file``. The allowed\nnumber of non matching lines can be set with ``lines_diff`` and the history\ndataset is sorted if ``sort`` is set to ``true``.\n- ``re_match_multiline``: it is checked if the history data sets matches the\nmulti line regular expression given in ``file``. The history dataset is sorted\nbefore the comparison if the ``sort`` atrribute is set to ``true``.\n- ``contains``: check if each line in ``file`` is contained in the history data set.\nThe allowed number of lines that are not contained in the history dataset\ncan be set with ``lines_diff``.\n- ``sim_size``: compares the size of the history dataset and the ``file`` subject to\nthe values of the ``delta`` and ``delta_frac`` attributes. Note that a ``has_size``\ncontent assertion should be preferred, because this avoids storing the test file.", + "properties": { + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" + } + ], + "default": null, + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + } + }, + "title": "TestOutput", + "type": "object" + }, + "TestOutputCollection": { + "additionalProperties": false, + "properties": { + "class": { + "anyOf": [ + { + "const": "Collection" + }, + { + "type": "null" + } + ], + "default": "Collection", + "title": "Class" + }, + "element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of elements in output collection.", + "title": "Count" + } + }, + "title": "TestOutputCollection", + "type": "object" + }, + "TestOutputCollectionDeprecated": { + "additionalProperties": false, + "properties": { + "element_tests": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + } + ] + }, + "description": "Deprecated field, please use elements to describe expectations about collection elements.", + "metadata": { + "deprecated": true + }, + "title": "Element Tests", + "type": "object" + }, + "collection_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Collection Type" + }, + "class": { + "anyOf": [ + { + "const": "Collection" + }, + { + "type": "null" + } + ], + "default": "Collection", + "title": "Class" + }, + "element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of elements in output collection.", + "title": "Count" + } + }, + "required": ["element_tests"], + "title": "TestOutputCollectionDeprecated", + "type": "object" + }, + "TestOutputCompareType": { + "description": "Type of comparison to use when comparing test generated output files to\nexpected output files.\n\nCurrently valid value are\n``diff`` (the default), ``re_match``, ``re_match_multiline``,\nand ``contains``. In addition there is ``sim_size`` which is discouraged in favour of a ``has_size`` assertion.", + "enum": ["diff", "re_match", "sim_size", "re_match_multiline", "contains"], + "title": "TestOutputCompareType", + "type": "string" + }, + "TestOutputElement": { + "additionalProperties": false, + "properties": { + "elements": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + } + ] + }, + "title": "Elements", + "type": "object" + }, + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" + } + ], + "default": null, + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + } + }, + "required": ["elements"], + "title": "TestOutputElement", + "type": "object" + }, + "TestOutputMetadata": { + "additionalProperties": false, + "description": "This directive specifies a test for an output's metadata as an expected key-\nvalue pair.\n\n### Example\nThe functional test tool\n[tool_provided_metadata_1.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/tool_provided_metadata_1.xml)\nprovides a demonstration of using this tag.\n```xml\n<test>\n<param name=\"input1\" value=\"simple_line.txt\" />\n<output name=\"out1\" file=\"simple_line.txt\" ftype=\"txt\">\n<metadata name=\"name\" value=\"my dynamic name\" />\n<metadata name=\"info\" value=\"my dynamic info\" />\n<metadata name=\"dbkey\" value=\"cust1\" />\n</output>\n</test>\n```", + "properties": { + "name": { + "description": "Name of the metadata element to check.", + "title": "Name", + "type": "string" + }, + "value": { + "description": "Expected value (as a string) of metadata value.", + "title": "Value", + "type": "string" + } + }, + "required": ["name", "value"], + "title": "TestOutputMetadata", + "type": "object" + } + }, + "items": { + "$ref": "#/$defs/Test" + }, + "title": "ListOfTests", + "type": "array" +}