Skip to content

Commit 342c713

Browse files
authored
GLSP-891: Json operation handler API (#59)
- Provides generic,reusable operation handler implementations for JSON-based source models - Refactor `RecordingCommand` API - Refactor `CreationOperationHandler` interfaces to be more strict (Previously it was possible to declare a handler with mixed (nodes/edges) types) Also: Filter out non-critical warning in webpack config Fixes eclipse-glsp/glsp#891
1 parent abdebbd commit 342c713

15 files changed

+232
-63
lines changed

examples/workflow-server/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ module.exports = env => {
5757
}
5858
]
5959
},
60-
ignoreWarnings: [/Failed to parse source map/]
60+
ignoreWarnings: [/Failed to parse source map/, /Can't resolve .* in '.*ws\/lib'/]
6161
};
6262
};

packages/server/src/common/command/recording-command.spec.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { AnyObject } from '@eclipse-glsp/protocol';
17+
import { AnyObject, MaybePromise } from '@eclipse-glsp/protocol';
1818
import { expect } from 'chai';
19-
import { RecordingCommand } from './recording-command';
19+
import { AbstractRecordingCommand } from './recording-command';
2020

2121
interface TestModel {
2222
string: string;
@@ -27,6 +27,16 @@ interface TestModel {
2727

2828
let jsonObject: TestModel;
2929

30+
class TestRecordingCommand<JsonObject extends AnyObject = AnyObject> extends AbstractRecordingCommand<JsonObject> {
31+
constructor(protected jsonObject: JsonObject, protected doExecute: () => MaybePromise<void>) {
32+
super();
33+
}
34+
35+
protected getJsonObject(): MaybePromise<JsonObject> {
36+
return this.jsonObject;
37+
}
38+
}
39+
3040
describe('RecordingCommand', () => {
3141
let beforeState: TestModel;
3242

@@ -41,14 +51,14 @@ describe('RecordingCommand', () => {
4151

4252
it('should be undoable after execution', async () => {
4353
// eslint-disable-next-line @typescript-eslint/no-empty-function
44-
const command = new RecordingCommand(jsonObject, () => {});
54+
const command = new TestRecordingCommand(jsonObject, () => {});
4555
expect(command.canUndo()).to.be.false;
4656
await command.execute();
4757
expect(command.canUndo()).to.be.true;
4858
});
4959

5060
it('should restore the pre execution state when undo is called', async () => {
51-
const command = new RecordingCommand(jsonObject, () => {
61+
const command = new TestRecordingCommand(jsonObject, () => {
5262
jsonObject.string = 'bar';
5363
jsonObject.flag = false;
5464
jsonObject.maybe = { hello: 'world' };
@@ -60,7 +70,7 @@ describe('RecordingCommand', () => {
6070
});
6171

6272
it('should restore the post execution state when redo is called', async () => {
63-
const command = new RecordingCommand(jsonObject, () => {
73+
const command = new TestRecordingCommand(jsonObject, () => {
6474
jsonObject.string = 'bar';
6575
jsonObject.flag = false;
6676
jsonObject.maybe = { hello: 'world' };

packages/server/src/common/command/recording-command.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
********************************************************************************/
1616

1717
import { GModelRootSchema } from '@eclipse-glsp/graph';
18-
import { AnyObject, MaybePromise } from '@eclipse-glsp/protocol';
18+
import { AnyObject, MaybePromise, SModelRootSchema } from '@eclipse-glsp/protocol';
1919
import * as jsonPatch from 'fast-json-patch';
2020
import { GModelSerializer } from '../features/model/gmodel-serializer';
2121
import { ModelState } from '../features/model/model-state';
@@ -34,6 +34,7 @@ export abstract class AbstractRecordingCommand<JsonObject extends AnyObject> imp
3434
const afterState = await this.getJsonObject();
3535
this.undoPatch = jsonPatch.compare(afterState, beforeState);
3636
this.redoPatch = jsonPatch.compare(beforeState, afterState);
37+
this.postChange?.(afterState);
3738
}
3839

3940
/**
@@ -48,8 +49,8 @@ export abstract class AbstractRecordingCommand<JsonObject extends AnyObject> imp
4849
*/
4950
protected abstract doExecute(): MaybePromise<void>;
5051

51-
protected applyPatch(object: JsonObject, patch: jsonPatch.Operation[]): MaybePromise<void> {
52-
jsonPatch.applyPatch(object, patch, false, true);
52+
protected applyPatch(object: JsonObject, patch: jsonPatch.Operation[]): jsonPatch.PatchResult<JsonObject> {
53+
return jsonPatch.applyPatch(object, patch, false, true);
5354
}
5455

5556
/**
@@ -64,33 +65,28 @@ export abstract class AbstractRecordingCommand<JsonObject extends AnyObject> imp
6465

6566
async undo(): Promise<void> {
6667
if (this.undoPatch) {
67-
return this.applyPatch(await this.getJsonObject(), this.undoPatch);
68+
const result = this.applyPatch(await this.getJsonObject(), this.undoPatch);
69+
this.postChange?.(result.newDocument);
6870
}
6971
}
7072

7173
async redo(): Promise<void> {
7274
if (this.redoPatch) {
73-
return this.applyPatch(await this.getJsonObject(), this.redoPatch);
75+
const result = this.applyPatch(await this.getJsonObject(), this.redoPatch);
76+
this.postChange?.(result.newDocument);
7477
}
7578
}
7679

7780
canUndo(): boolean {
7881
return !!this.undoPatch && !!this.redoPatch;
7982
}
80-
}
81-
82-
/**
83-
* Simple base implementation of {@link AbstractRecordingCommand} that records the changes made to the given JSON object during
84-
* the the given `doExecute` function.
85-
*/
86-
export class RecordingCommand<JsonObject extends AnyObject = AnyObject> extends AbstractRecordingCommand<JsonObject> {
87-
constructor(protected jsonObject: JsonObject, protected doExecute: () => MaybePromise<void>) {
88-
super();
89-
}
9083

91-
protected getJsonObject(): MaybePromise<JsonObject> {
92-
return this.jsonObject;
93-
}
84+
/**
85+
* Optional hook that (if implemented) will be
86+
* executed after every command-driven action that changed
87+
* the underlying model i.e. command execute, undo and redo.
88+
*/
89+
protected postChange?(newModel: JsonObject): MaybePromise<void>;
9490
}
9591

9692
/**
@@ -102,18 +98,12 @@ export class GModelRecordingCommand extends AbstractRecordingCommand<GModelRootS
10298
super();
10399
}
104100

105-
override async execute(): Promise<void> {
106-
await super.execute();
107-
this.modelState.index.indexRoot(this.modelState.root);
108-
}
109-
110101
protected getJsonObject(): MaybePromise<GModelRootSchema> {
111102
return this.serializer.createSchema(this.modelState.root);
112103
}
113104

114-
protected override async applyPatch(rootSchema: GModelRootSchema, patch: jsonPatch.Operation[]): Promise<void> {
115-
await super.applyPatch(rootSchema, patch);
116-
const newRoot = this.serializer.createRoot(rootSchema);
105+
protected override postChange(newModel: SModelRootSchema): MaybePromise<void> {
106+
const newRoot = this.serializer.createRoot(newModel);
117107
this.modelState.updateRoot(newRoot);
118108
}
119109
}

packages/server/src/common/features/model/model-state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export class DefaultModelState implements ModelState {
9292
}
9393

9494
updateRoot(newRoot: GModelRoot): void {
95+
if (!newRoot.revision && this.root) {
96+
newRoot.revision = this.root.revision;
97+
}
9598
this.root = newRoot;
9699
this.index.indexRoot(newRoot);
97100
}

packages/server/src/common/features/model/model-submission-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ export class ModelSubmissionHandler {
110110

111111
const revision = this.requestModelAction ? 0 : this.modelState.root.revision! + 1;
112112
this.modelState.root.revision = revision;
113-
const root = this.serializeGModel();
114113

115114
if (this.diagramConfiguration.needsClientLayout) {
115+
const root = this.serializeGModel();
116116
return [RequestBoundsAction.create(root), SetDirtyStateAction.create(this.commandStack.isDirty, { reason })];
117117
}
118118
return this.submitModelDirectly(reason);

packages/server/src/common/gmodel/gmodel-create-edge-operation-handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { GNode } from '@eclipse-glsp/graph/lib/gnode';
1919
import { CreateEdgeOperation, MaybePromise, TriggerEdgeCreationAction } from '@eclipse-glsp/protocol';
2020
import { injectable } from 'inversify';
2121
import { Command } from '../command/command';
22-
import { CreateOperationHandler, CreateOperationKind } from '../operations/create-operation-handler';
22+
import { CreateEdgeOperationHandler } from '../operations/create-operation-handler';
2323
import { GModelOperationHandler } from './gmodel-operation-handler';
2424

2525
/**
@@ -28,8 +28,8 @@ import { GModelOperationHandler } from './gmodel-operation-handler';
2828
* (i.e. all operation handlers directly modify the graphical model).
2929
*/
3030
@injectable()
31-
export abstract class GModelCreateEdgeOperationHandler extends GModelOperationHandler implements CreateOperationHandler {
32-
override readonly operationType: CreateOperationKind = CreateEdgeOperation.KIND;
31+
export abstract class GModelCreateEdgeOperationHandler extends GModelOperationHandler implements CreateEdgeOperationHandler {
32+
override readonly operationType = CreateEdgeOperation.KIND;
3333
abstract override label: string;
3434
abstract elementTypeIds: string[];
3535

packages/server/src/common/gmodel/gmodel-create-node-operation-handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { CreateNodeOperation, MaybePromise, Point, SelectAction, TriggerNodeCrea
1919
import { inject, injectable } from 'inversify';
2020
import { ActionDispatcher } from '../actions/action-dispatcher';
2121
import { Command } from '../command/command';
22-
import { CreateOperationHandler, CreateOperationKind } from '../operations/create-operation-handler';
22+
import { CreateNodeOperationHandler } from '../operations/create-operation-handler';
2323
import { getRelativeLocation } from '../utils/layout-util';
2424
import { GModelOperationHandler } from './gmodel-operation-handler';
2525

@@ -29,15 +29,15 @@ import { GModelOperationHandler } from './gmodel-operation-handler';
2929
* (i.e. all operation handlers directly modify the graphical model).
3030
*/
3131
@injectable()
32-
export abstract class GModelCreateNodeOperationHandler extends GModelOperationHandler implements CreateOperationHandler {
32+
export abstract class GModelCreateNodeOperationHandler extends GModelOperationHandler implements CreateNodeOperationHandler {
3333
@inject(ActionDispatcher)
3434
protected actionDispatcher: ActionDispatcher;
3535

3636
abstract elementTypeIds: string[];
3737

3838
abstract override label: string;
3939

40-
override operationType: CreateOperationKind = CreateNodeOperation.KIND;
40+
override readonly operationType = CreateNodeOperation.KIND;
4141

4242
override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
4343
return this.commandOf(() => this.executeCreation(operation));

packages/server/src/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export * from './gmodel/index';
7171
export * from './launch/glsp-server-launcher';
7272
export * from './operations/compound-operation-handler';
7373
export * from './operations/create-operation-handler';
74+
export * from './operations/json-operation-handler';
7475
export * from './operations/operation-action-handler';
7576
export * from './operations/operation-handler';
7677
export * from './operations/operation-handler-registry';

packages/server/src/common/operations/compound-operation-handler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import { Command, CompoundCommand } from '../command/command';
1919
import { OperationHandler } from './operation-handler';
2020
import { OperationHandlerRegistry } from './operation-handler-registry';
2121

22+
/**
23+
* Generic {@link OperationHandler} from {@link CompoundOperations}.
24+
* Retrieves the corresponding execution commands for the list of (sub) operations
25+
* and constructs a {@link CompoundCommand} for them.
26+
*/
2227
@injectable()
2328
export class CompoundOperationHandler extends OperationHandler {
2429
@inject(OperationHandlerRegistry)
@@ -27,7 +32,7 @@ export class CompoundOperationHandler extends OperationHandler {
2732
operationType = CompoundOperation.KIND;
2833

2934
async createCommand(operation: CompoundOperation): Promise<Command | undefined> {
30-
const maybeCommands = operation.operationList.map(op => this.operationHandlerRegistry.getExecutableCommand(op));
35+
const maybeCommands = operation.operationList.map(op => this.operationHandlerRegistry.getOperationHandler(op)?.execute(op));
3136
const commands: Command[] = [];
3237

3338
for await (const command of maybeCommands) {

packages/server/src/common/operations/create-operation-handler.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,48 @@ import {
1919
hasArrayProp,
2020
hasFunctionProp,
2121
hasStringProp,
22+
MaybePromise,
2223
TriggerEdgeCreationAction,
2324
TriggerNodeCreationAction
2425
} from '@eclipse-glsp/protocol';
26+
import { Command } from '../command/command';
2527
import { OperationHandler } from './operation-handler';
2628

2729
/**
28-
* A special {@link OperationHandler} that is responsible for the handling of {@link CreateOperation}s. Depending on its
30+
* A special {@link OperationHandler} that is responsible for the handling of (a subset of) {@link CreateEdgeOperation}s. Depending on its
2931
* operation type the triggered actions are {@link TriggerNodeCreationAction} or {@link TriggerEdgeCreationAction}s.
3032
*/
31-
export interface CreateOperationHandler extends OperationHandler {
33+
export interface CreateEdgeOperationHandler extends OperationHandler {
34+
label: string;
35+
elementTypeIds: string[];
36+
operationType: typeof CreateEdgeOperation.KIND;
37+
getTriggerActions(): TriggerEdgeCreationAction[];
38+
createCommand(operation: CreateEdgeOperation): MaybePromise<Command | undefined>;
39+
}
40+
41+
export interface CreateNodeOperationHandler extends OperationHandler {
3242
readonly label: string;
3343
elementTypeIds: string[];
34-
operationType: CreateOperationKind;
35-
getTriggerActions(): (TriggerEdgeCreationAction | TriggerNodeCreationAction)[];
44+
operationType: typeof CreateNodeOperation.KIND;
45+
getTriggerActions(): TriggerNodeCreationAction[];
46+
createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined>;
3647
}
48+
/**
49+
* A special {@link OperationHandler} that is responsible for the handling of a node or edge creation operation. Depending on its
50+
* operation type the triggered actions are {@link TriggerNodeCreationAction} or {@link TriggerEdgeCreationAction}s.
51+
*/
52+
export type CreateOperationHandler = CreateNodeOperationHandler | CreateEdgeOperationHandler;
3753

3854
export type CreateOperationKind = typeof CreateNodeOperation.KIND | typeof CreateEdgeOperation.KIND;
3955

4056
export namespace CreateOperationHandler {
4157
export function is(object: unknown): object is CreateOperationHandler {
4258
return (
4359
object instanceof OperationHandler &&
60+
hasStringProp(object, 'operationType') &&
4461
hasStringProp(object, 'label') &&
4562
hasArrayProp(object, 'elementTypeIds') &&
46-
hasFunctionProp(object, 'getTriggerActions')
63+
hasFunctionProp(object, 'getTriggerActions', true)
4764
);
4865
}
4966
}

0 commit comments

Comments
 (0)