From 82d049f58f536aca485fb60770765769c59bf771 Mon Sep 17 00:00:00 2001
From: Haydar Metin <hmetin@eclipsesource.com>
Date: Mon, 12 Aug 2024 11:58:51 +0200
Subject: [PATCH] Fix java server differences (#22)

* Fix java server differences
* Make IntegrationVariable type safe
---
 .github/workflows/ci.yml                      |  3 +-
 .github/workflows/e2e.yml                     |  1 +
 examples/workflow-test/.env.example           |  1 +
 examples/workflow-test/src/server/index.ts    | 18 ++++
 .../command-palette/command-palette.spec.ts   | 61 +++++++++----
 .../tests/features/hover/popup.spec.ts        | 73 ++++++++++-----
 .../validation/marker-navigator.spec.ts       | 30 +++----
 .../glsp-playwright/src/glsp-server/index.ts  | 17 ++++
 .../glsp-playwright/src/glsp-server/server.ts | 21 +++++
 packages/glsp-playwright/src/index.ts         |  1 +
 packages/glsp-playwright/src/test.ts          | 26 +++---
 .../src/test/dynamic-variable.ts              | 89 +++++++++++++++++++
 12 files changed, 266 insertions(+), 75 deletions(-)
 create mode 100644 examples/workflow-test/src/server/index.ts
 create mode 100644 packages/glsp-playwright/src/glsp-server/index.ts
 create mode 100644 packages/glsp-playwright/src/glsp-server/server.ts
 create mode 100644 packages/glsp-playwright/src/test/dynamic-variable.ts

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c1d7dd6..453f47f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -56,6 +56,7 @@ jobs:
     env:
       THEIA_URL: 'http://localhost:3000'
       VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example'
+      GLSP_SERVER_TYPE: 'node'
       GLSP_SERVER_DEBUG: 'true'
       GLSP_SERVER_PORT: '8081'
       GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true'
@@ -87,13 +88,13 @@ jobs:
           path: examples/workflow-test/playwright-report/
 
   playwright-java:
-    if: false
     name: Playwright Tests (Java server)
     timeout-minutes: 120
     runs-on: ubuntu-latest
     env:
       THEIA_URL: 'http://localhost:3000'
       VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example'
+      GLSP_SERVER_TYPE: 'java'
       GLSP_SERVER_DEBUG: 'true'
       GLSP_SERVER_PORT: '8081'
       GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true'
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 9067be9..94a863f 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -21,6 +21,7 @@ jobs:
     env:
       THEIA_URL: 'http://localhost:3000'
       VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example'
+      GLSP_SERVER_TYPE: 'node'
       GLSP_SERVER_DEBUG: 'true'
       GLSP_SERVER_PORT: '8081'
       GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true'
diff --git a/examples/workflow-test/.env.example b/examples/workflow-test/.env.example
index 3a83020..854c92e 100644
--- a/examples/workflow-test/.env.example
+++ b/examples/workflow-test/.env.example
@@ -2,6 +2,7 @@ GLSP_SERVER_DEBUG="true" # For Theia and VSCode instances to connect to an exter
 GLSP_SERVER_PORT="8081"
 GLSP_SERVER_PLAYWRIGHT_MANAGED="true" # To allow Playwright to manage/start the server
 GLSP_WEBSOCKET_PATH="workflow"
+GLSP_SERVER_TYPE="node" # To use the node server
 
 # Configurations
 STANDALONE_URL="file:///.../repositories/glsp-client/examples/workflow-standalone/app/diagram.html"
diff --git a/examples/workflow-test/src/server/index.ts b/examples/workflow-test/src/server/index.ts
new file mode 100644
index 0000000..efcdd9e
--- /dev/null
+++ b/examples/workflow-test/src/server/index.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * Copyright (c) 2024 EclipseSource and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export const GLSP_SERVER_TYPE_NODE = 'node';
+export const GLSP_SERVER_TYPE_JAVA = 'java';
diff --git a/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
index 0290171..581f7c5 100644
--- a/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
+++ b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
@@ -14,11 +14,14 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 import { GLSPGlobalCommandPalette, expect, test } from '@eclipse-glsp/glsp-playwright/';
+import { GLSPServer } from '@eclipse-glsp/glsp-playwright/src/glsp-server';
+import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
 import { WorkflowApp } from '../../../src/app/workflow-app';
 import { Edge } from '../../../src/graph/elements/edge.po';
 import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
 import { TaskManual } from '../../../src/graph/elements/task-manual.po';
 import { WorkflowGraph } from '../../../src/graph/workflow.graph';
+import { GLSP_SERVER_TYPE_JAVA, GLSP_SERVER_TYPE_NODE } from '../../../src/server';
 
 const element1Label = 'Push';
 const element2Label = 'ChkWt';
@@ -27,14 +30,16 @@ test.describe('The command palette', () => {
     let app: WorkflowApp;
     let graph: WorkflowGraph;
     let globalCommandPalette: GLSPGlobalCommandPalette;
+    let server: GLSPServer;
 
-    test.beforeEach(async ({ integration }) => {
+    test.beforeEach(async ({ integration, glspServer }) => {
         app = new WorkflowApp({
             type: 'integration',
             integration
         });
         graph = app.graph;
         globalCommandPalette = app.globalCommandPalette;
+        server = glspServer;
     });
 
     test.describe('in the global context', () => {
@@ -45,23 +50,43 @@ test.describe('The command palette', () => {
             expect(await globalCommandPalette.isVisible()).toBeTruthy();
 
             const suggestions = await globalCommandPalette.suggestions();
-            const expectedSuggestions = [
-                'Create Manual Task',
-                'Create Category',
-                'Create Automated Task',
-                'Create Merge Node',
-                'Create Decision Node',
-                'Delete All',
-                'Reveal Push',
-                'Reveal ChkWt',
-                'Reveal WtOK',
-                'Reveal RflWt',
-                'Reveal Brew',
-                'Reveal ChkTp',
-                'Reveal KeepTp',
-                'Reveal PreHeat'
-            ];
-            expect(suggestions.sort()).toEqual(expectedSuggestions.sort());
+            const expectedSuggestions = new ServerVariable({
+                server,
+                value: {
+                    [GLSP_SERVER_TYPE_NODE]: [
+                        'Create Manual Task',
+                        'Create Category',
+                        'Create Automated Task',
+                        'Create Merge Node',
+                        'Create Decision Node',
+                        'Delete All',
+                        'Reveal Push',
+                        'Reveal ChkWt',
+                        'Reveal WtOK',
+                        'Reveal RflWt',
+                        'Reveal Brew',
+                        'Reveal ChkTp',
+                        'Reveal KeepTp',
+                        'Reveal PreHeat'
+                    ],
+                    [GLSP_SERVER_TYPE_JAVA]: [
+                        'Create Manual Task',
+                        'Create Category',
+                        'Create Automated Task',
+                        'Create Merge Node',
+                        'Create Decision Node',
+                        'Reveal Push',
+                        'Reveal ChkWt',
+                        'Reveal WtOK',
+                        'Reveal RflWt',
+                        'Reveal Brew',
+                        'Reveal ChkTp',
+                        'Reveal KeepTp',
+                        'Reveal PreHeat'
+                    ]
+                }
+            });
+            expect(suggestions.sort()).toEqual(expectedSuggestions.get().sort());
 
             await globalCommandPalette.search('Create');
             const createSuggestions = await globalCommandPalette.suggestions();
diff --git a/examples/workflow-test/tests/features/hover/popup.spec.ts b/examples/workflow-test/tests/features/hover/popup.spec.ts
index d2b5238..bfe3394 100644
--- a/examples/workflow-test/tests/features/hover/popup.spec.ts
+++ b/examples/workflow-test/tests/features/hover/popup.spec.ts
@@ -23,37 +23,64 @@ import {
     test
 } from '@eclipse-glsp/glsp-playwright/';
 import { PLabelledElement } from '@eclipse-glsp/glsp-playwright/src/extension';
+import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
 import { dedent } from 'ts-dedent';
 import { WorkflowApp } from '../../../src/app/workflow-app';
 import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
 import { TaskManual } from '../../../src/graph/elements/task-manual.po';
 import { WorkflowGraph } from '../../../src/graph/workflow.graph';
+import { GLSP_SERVER_TYPE_JAVA, GLSP_SERVER_TYPE_NODE } from '../../../src/server';
 
 const manualLabel = 'Push';
-const expectedManualPopupText = dedent`Push
-Type: manual
-Duration: undefined
-Reference: undefined
+const expectedManualPopupText = new ServerVariable({
+    value: {
+        [GLSP_SERVER_TYPE_NODE]: dedent`Push
+        Type: manual
+        Duration: undefined
+        Reference: undefined
+        
+        `,
+        [GLSP_SERVER_TYPE_JAVA]: dedent`Push
+
+        Type: manual
+        Duration: 0
+        Reference: null
+        
+        `
+    }
+});
 
-`;
 const automatedLabel = 'ChkWt';
-const expectedAutomatedPopupText = dedent`ChkWt
-Type: automated
-Duration: undefined
-Reference: undefined
-
-`;
+const expectedAutomatedPopupText = new ServerVariable({
+    value: {
+        [GLSP_SERVER_TYPE_NODE]: dedent`ChkWt
+        Type: automated
+        Duration: undefined
+        Reference: undefined
+        
+        `,
+        [GLSP_SERVER_TYPE_JAVA]: dedent`ChkWt
+
+        Type: automated
+        Duration: 0
+        Reference: null
+        
+        `
+    }
+});
 
 test.describe('The popup', () => {
     let app: WorkflowApp;
     let graph: WorkflowGraph;
 
-    test.beforeEach(async ({ integration }) => {
+    test.beforeEach(async ({ integration, glspServer }) => {
         app = new WorkflowApp({
             type: 'integration',
             integration
         });
         graph = app.graph;
+        expectedManualPopupText.setServer(glspServer);
+        expectedAutomatedPopupText.setServer(glspServer);
     });
 
     test('should be shown on hovering a task manual', async () => {
@@ -65,7 +92,7 @@ test.describe('The popup', () => {
         await expect(app.popup.locate()).toBeVisible();
 
         const popup = task.popup();
-        expect(await popup.innerText()).toBe(expectedManualPopupText);
+        expect(await popup.innerText()).toBe(expectedManualPopupText.get());
     });
 
     test('should allow to access the text directly in elements', async () => {
@@ -73,13 +100,13 @@ test.describe('The popup', () => {
         await expect(app.popup.locate()).toBeHidden();
         const text = await task.popupText();
         await expect(app.popup.locate()).toBeVisible();
-        expect(text).toBe(expectedManualPopupText);
+        expect(text).toBe(expectedManualPopupText.get());
     });
 
     test.describe('should be closed on', () => {
         test('escape', async () => {
             await app.graph.focus();
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
 
             await app.page.keyboard.press('Escape');
             await app.popup.waitForHidden();
@@ -88,15 +115,15 @@ test.describe('The popup', () => {
         });
 
         test('new hover', async () => {
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
 
             await app.popup.close();
 
-            await assertPopup(app, automatedLabel, TaskAutomated, expectedAutomatedPopupText);
+            await assertPopup(app, automatedLabel, TaskAutomated, expectedAutomatedPopupText.get());
         });
 
         test('mouse moved away', async () => {
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
 
             const bounds = await app.graph.bounds();
             await bounds.position('middle_center').move();
@@ -106,7 +133,7 @@ test.describe('The popup', () => {
         });
 
         test('focus lost', async () => {
-            const task = await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            const task = await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
 
             await app.graph.locate().click();
             await app.popup.waitForHidden();
@@ -116,7 +143,7 @@ test.describe('The popup', () => {
 
         test('context menu', async ({ integrationOptions }) => {
             test.skip(skipNonIntegration(integrationOptions, 'Theia'), 'Only within Theia supported');
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
 
             await app.contextMenu.open();
             await app.popup.waitForHidden();
@@ -125,7 +152,7 @@ test.describe('The popup', () => {
         });
 
         test('center command', async ({ integration }) => {
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
             await graph.focus();
             await runInIntegration(
                 integration,
@@ -143,7 +170,7 @@ test.describe('The popup', () => {
         });
 
         test('fit to screen command', async ({ integration }) => {
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
             await graph.focus();
             await runInIntegration(
                 integration,
@@ -161,7 +188,7 @@ test.describe('The popup', () => {
         });
 
         test('layout command', async ({ integration }) => {
-            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText);
+            await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get());
             await graph.focus();
             await runInIntegration(
                 integration,
diff --git a/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts b/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
index 62c2f0d..c27ffee 100644
--- a/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
+++ b/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
@@ -13,15 +13,9 @@
  *
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
-import {
-    MarkerNavigator,
-    StandaloneMarkerNavigator,
-    TheiaMarkerNavigator,
-    createParameterizedIntegrationData,
-    expect,
-    test
-} from '@eclipse-glsp/glsp-playwright/';
+import { MarkerNavigator, StandaloneMarkerNavigator, TheiaMarkerNavigator, expect, test } from '@eclipse-glsp/glsp-playwright/';
 import { skipIntegration } from '@eclipse-glsp/glsp-playwright/src/test';
+import { IntegrationVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
 import { WorkflowApp } from '../../../src/app/workflow-app';
 import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
 import { WorkflowGraph } from '../../../src/graph/workflow.graph';
@@ -47,24 +41,24 @@ test.describe('The marker navigator', () => {
             integration
         });
         graph = app.graph;
-        navigator = createParameterizedIntegrationData<() => MarkerNavigator>({
-            default: () => {
-                throw new Error('Integration not supported for marker navigator');
+        const navigatorProvider = new IntegrationVariable<MarkerNavigator>({
+            value: {
+                Standalone: new StandaloneMarkerNavigator(app),
+                Theia: new TheiaMarkerNavigator(app)
             },
-            override: {
-                Standalone: () => new StandaloneMarkerNavigator(app),
-                Theia: () => new TheiaMarkerNavigator(app)
-            }
-        })[integration.type]();
+            integration
+        });
+
+        navigator = navigatorProvider.get();
 
         await navigator.trigger();
     });
 
     // VSCode has no support for navigation
-    test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'VSCode'));
+    test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'VSCode'), 'Not supported');
 
     test('should navigate to the first element', async ({ integrationOptions }) => {
-        test.skip(skipIntegration(integrationOptions, 'VSCode'));
+        test.skip(skipIntegration(integrationOptions, 'VSCode'), 'Not supported');
 
         await navigator.navigateForward();
         await app.page.pause();
diff --git a/packages/glsp-playwright/src/glsp-server/index.ts b/packages/glsp-playwright/src/glsp-server/index.ts
new file mode 100644
index 0000000..ddb9881
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp-server/index.ts
@@ -0,0 +1,17 @@
+/********************************************************************************
+ * Copyright (c) 2024 EclipseSource and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export * from './server';
diff --git a/packages/glsp-playwright/src/glsp-server/server.ts b/packages/glsp-playwright/src/glsp-server/server.ts
new file mode 100644
index 0000000..e64f08a
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp-server/server.ts
@@ -0,0 +1,21 @@
+/********************************************************************************
+ * Copyright (c) 2024 EclipseSource and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export interface GLSPServer {
+    type: string;
+}
+
+export const GLSP_SERVER_TYPE_UNKNWON = 'unknown';
diff --git a/packages/glsp-playwright/src/index.ts b/packages/glsp-playwright/src/index.ts
index 0956412..c942647 100644
--- a/packages/glsp-playwright/src/index.ts
+++ b/packages/glsp-playwright/src/index.ts
@@ -17,6 +17,7 @@
 export * from './debug';
 export * from './extension';
 export * from './glsp';
+export * from './glsp-server';
 export * from './integration';
 export * from './remote';
 export * from './test';
diff --git a/packages/glsp-playwright/src/test.ts b/packages/glsp-playwright/src/test.ts
index c464909..a4aab90 100644
--- a/packages/glsp-playwright/src/test.ts
+++ b/packages/glsp-playwright/src/test.ts
@@ -25,6 +25,7 @@ import {
     VSCodeIntegration,
     VSCodeSetup
 } from '~/integration';
+import { GLSP_SERVER_TYPE_UNKNWON, GLSPServer } from './glsp-server';
 
 /**
  * GLSP-Playwright specific options
@@ -56,6 +57,7 @@ export interface GLSPPlaywrightFixtures {
      * ```
      */
     integration: Integration;
+    glspServer: GLSPServer;
     vscodeSetup?: VSCodeSetup;
 }
 
@@ -70,6 +72,14 @@ export const test = base.extend<GLSPPlaywrightOptions & GLSPPlaywrightFixtures>(
         }
     },
 
+    glspServer: async ({ page }, use) => {
+        const server: GLSPServer = {
+            type: process.env.GLSP_SERVER_TYPE ?? GLSP_SERVER_TYPE_UNKNWON
+        };
+
+        await use(server);
+    },
+
     integration: async ({ playwright, browser, page, integrationOptions }, use) => {
         const args: IntegrationArgs = {
             playwright,
@@ -111,21 +121,6 @@ export const test = base.extend<GLSPPlaywrightOptions & GLSPPlaywrightFixtures>(
     }
 });
 
-// https://playwright.dev/docs/test-parameterize
-
-export type ParameterizedIntegrationData<T> = Record<IntegrationType, T>;
-export function createParameterizedIntegrationData<T>(options: {
-    default: T;
-    override?: Partial<ParameterizedIntegrationData<T>>;
-}): ParameterizedIntegrationData<T> {
-    return {
-        Page: options.override?.Page ?? options.default,
-        Standalone: options.override?.Standalone ?? options.default,
-        Theia: options.override?.Theia ?? options.default,
-        VSCode: options.override?.VSCode ?? options.default
-    };
-}
-
 /**
  * Runs the given callback if the active integration is the same as the provided integration type
  */
@@ -173,4 +168,5 @@ export function skipIntegration(integrationOptions?: IntegrationOptions, ...inte
 }
 
 export { expect } from './test/assertions';
+export { DynamicVariable } from './test/dynamic-variable';
 export { test as setup };
diff --git a/packages/glsp-playwright/src/test/dynamic-variable.ts b/packages/glsp-playwright/src/test/dynamic-variable.ts
new file mode 100644
index 0000000..e5624cb
--- /dev/null
+++ b/packages/glsp-playwright/src/test/dynamic-variable.ts
@@ -0,0 +1,89 @@
+/********************************************************************************
+ * Copyright (c) 2024 EclipseSource and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { Integration, IntegrationType } from '~/integration';
+import type { GLSPServer } from '../glsp-server';
+
+type RecordKey = string | number | symbol;
+
+export interface DynamicVariableOptions<TKey extends RecordKey, TValue> {
+    defaultValue?: TValue;
+    value?: Partial<Record<TKey, TValue | undefined>>;
+}
+
+export interface DynamicVariable<TKey extends RecordKey, TValue> {
+    getOrThrow(key: TKey): TValue;
+}
+
+export abstract class BaseDynamicVariable<TKey extends RecordKey, TValue> {
+    protected readonly value: Partial<Record<TKey, TValue | undefined>>;
+    protected readonly defaultValue?: TValue;
+
+    constructor(protected readonly options: DynamicVariableOptions<TKey, TValue>) {
+        this.defaultValue = options?.defaultValue;
+        this.value = options?.value ?? {};
+    }
+
+    getOrThrow(key: TKey): TValue {
+        const value = this.value[key] ?? this.defaultValue;
+        if (value === undefined) {
+            throw new Error(`No value found for key ${String(key)}`);
+        }
+        return value;
+    }
+}
+
+export class IntegrationVariable<TValue> extends BaseDynamicVariable<IntegrationType, TValue> {
+    protected integration?: Integration;
+
+    constructor(options: DynamicVariableOptions<IntegrationType, TValue> & { integration?: Integration }) {
+        super(options);
+        this.integration = options.integration;
+    }
+
+    get(): TValue {
+        if (this.integration === undefined) {
+            throw new Error('No integration set');
+        }
+
+        return super.getOrThrow(this.integration.type);
+    }
+
+    setIntegration(integration: Integration): void {
+        this.integration = integration;
+    }
+}
+
+export class ServerVariable<TValue> extends BaseDynamicVariable<string, TValue> {
+    protected server?: GLSPServer;
+
+    constructor(options: DynamicVariableOptions<string, TValue> & { server?: GLSPServer }) {
+        super(options);
+        this.server = options.server;
+    }
+
+    get(): TValue {
+        if (this.server === undefined) {
+            throw new Error('No GLSP server set');
+        }
+
+        return super.getOrThrow(this.server.type);
+    }
+
+    setServer(server: GLSPServer): void {
+        this.server = server;
+    }
+}