Skip to content

Commit 66f756a

Browse files
authored
Merge pull request #18 from davelopez/explore_schema_validation
Add basic JSON schema validation
2 parents 49b5431 + 5ef6ece commit 66f756a

8 files changed

Lines changed: 321 additions & 31 deletions

File tree

server/src/languageService.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
1-
import { ASTNode, getLanguageService, LanguageService } from "vscode-json-languageservice";
1+
import {
2+
getLanguageService,
3+
LanguageService,
4+
LanguageServiceParams,
5+
DocumentLanguageSettings,
6+
Diagnostic,
7+
JSONSchema,
8+
LanguageSettings,
9+
SchemaConfiguration,
10+
} from "vscode-json-languageservice";
211
import {
312
TextDocument,
413
Range,
514
FormattingOptions,
615
TextEdit,
716
WorkflowDocument,
817
WorkflowLanguageService,
18+
Position,
19+
Hover,
920
} from "./languageTypes";
21+
import NativeWorkflowSchema from "../../workflow-languages/schemas/native.schema.json";
1022

1123
/**
1224
* A wrapper around the JSON Language Service to support language features
1325
* for native Galaxy workflow files AKA '.ga' workflows.
1426
*/
1527
export class NativeWorkflowLanguageService implements WorkflowLanguageService {
1628
private _jsonLanguageService: LanguageService;
29+
private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" };
1730

1831
constructor() {
19-
this._jsonLanguageService = getLanguageService({});
32+
const params: LanguageServiceParams = {};
33+
const settings = this.getLanguageSettings();
34+
this._jsonLanguageService = getLanguageService(params);
35+
this._jsonLanguageService.configure(settings);
36+
}
37+
38+
public get schema(): JSONSchema {
39+
return NativeWorkflowSchema;
2040
}
2141

2242
public parseWorkflowDocument(document: TextDocument): WorkflowDocument {
@@ -27,8 +47,38 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService {
2747
public format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] {
2848
return this._jsonLanguageService.format(document, range, options);
2949
}
30-
}
3150

32-
export function getRange(document: TextDocument, node: ASTNode) {
33-
return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
51+
public async doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
52+
const schemaValidationResults = await this._jsonLanguageService.doValidation(
53+
workflowDocument.textDocument,
54+
workflowDocument.jsonDocument,
55+
this._documentSettings,
56+
this.schema
57+
);
58+
return schemaValidationResults;
59+
}
60+
61+
public async doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null> {
62+
const hover = await this._jsonLanguageService.doHover(
63+
workflowDocument.textDocument,
64+
position,
65+
workflowDocument.jsonDocument
66+
);
67+
return hover;
68+
}
69+
70+
private getLanguageSettings(): LanguageSettings {
71+
const settings: LanguageSettings = {
72+
schemas: [this.getWorkflowSchemaConfig()],
73+
};
74+
return settings;
75+
}
76+
77+
private getWorkflowSchemaConfig(): SchemaConfiguration {
78+
return {
79+
uri: this.schema.id ?? "",
80+
fileMatch: ["**.ga"],
81+
schema: this.schema,
82+
};
83+
}
3484
}

server/src/languageTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export interface FormattingOptions extends LSPFormattingOptions {
121121
export interface WorkflowLanguageService {
122122
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
123123
parseWorkflowDocument(document: TextDocument): WorkflowDocument;
124+
doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]>;
124125
}
125126

126127
export abstract class ServerContext {

server/src/models/workflowDocument.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { JSONDocument } from "vscode-json-languageservice";
2-
import { TextDocument } from "../languageTypes";
2+
import { TextDocument, Range, Position, ASTNode } from "../languageTypes";
33

44
/**
55
* This class contains information about workflow semantics.
@@ -25,4 +25,26 @@ export class WorkflowDocument {
2525
public get jsonDocument(): JSONDocument {
2626
return this._jsonDocument;
2727
}
28+
29+
public getNodeAtPosition(position: Position): ASTNode | undefined {
30+
const offset = this.textDocument.offsetAt(position);
31+
return this.jsonDocument.getNodeFromOffset(offset);
32+
}
33+
34+
public getNodeRange(node: ASTNode): Range {
35+
return Range.create(
36+
this.textDocument.positionAt(node.offset),
37+
this.textDocument.positionAt(node.offset + node.length)
38+
);
39+
}
40+
41+
public getNodeRangeAtPosition(position: Position): Range {
42+
const node = this.getNodeAtPosition(position);
43+
return node ? this.getNodeRange(node) : this.getDefaultRangeAtPosition(position);
44+
}
45+
46+
private getDefaultRangeAtPosition(position: Position): Range {
47+
const offset = this.textDocument.offsetAt(position);
48+
return Range.create(this.textDocument.positionAt(offset), this.textDocument.positionAt(offset + 1));
49+
}
2850
}

server/src/providers/hoverProvider.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { Hover, HoverParams, MarkupKind, Position, ASTNode, WorkflowDocument, PropertyASTNode } from "../languageTypes";
1+
import { Hover, HoverParams, MarkupKind, ASTNode, PropertyASTNode } from "../languageTypes";
22
import { GalaxyWorkflowLanguageServer } from "../server";
33
import { Provider } from "./provider";
4-
import { getRange } from "../languageService";
54
import { ArrayASTNode, BooleanASTNode, NullASTNode, NumberASTNode, StringASTNode } from "vscode-json-languageservice";
65

76
export class HoverProvider extends Provider {
@@ -23,12 +22,12 @@ export class HoverProvider extends Provider {
2322
if (!workflowDocument) {
2423
return undefined;
2524
}
26-
const node = this.getNodeAtDocumentPosition(workflowDocument, params.position);
25+
const node = workflowDocument.getNodeAtPosition(params.position);
2726
if (!node) {
2827
return undefined;
2928
}
3029
const contentLines = this.printNode(node);
31-
const hoverRange = getRange(workflowDocument.textDocument, node);
30+
const hoverRange = workflowDocument.getNodeRange(node);
3231
const markdown = {
3332
kind: MarkupKind.Markdown,
3433
value: contentLines.join("\n\n"),
@@ -40,13 +39,6 @@ export class HoverProvider extends Provider {
4039
return result;
4140
}
4241

43-
private getNodeAtDocumentPosition(workflowDocument: WorkflowDocument, position: Position): ASTNode | undefined {
44-
const document = workflowDocument.textDocument;
45-
const offset = document.offsetAt(position);
46-
const node = workflowDocument.jsonDocument.getNodeFromOffset(offset);
47-
return node;
48-
}
49-
5042
private printNode(node: ASTNode): string[] {
5143
const contentLines = [`## ${node.type}`];
5244
if (node.type === "object") {

server/src/providers/symbolsProvider.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { getRange } from "../languageService";
21
import {
3-
TextDocument,
42
DocumentSymbolParams,
53
DocumentSymbol,
64
SymbolKind,
75
ASTNode,
86
PropertyASTNode,
97
ObjectASTNode,
8+
WorkflowDocument,
109
} from "../languageTypes";
1110
import { GalaxyWorkflowLanguageServer } from "../server";
1211
import { Provider } from "./provider";
@@ -26,13 +25,14 @@ export class SymbolsProvider extends Provider {
2625
public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] {
2726
const workflowDocument = this.workflowDocuments.get(params.textDocument.uri);
2827
if (workflowDocument) {
29-
const symbols = this.getSymbols(workflowDocument.textDocument, workflowDocument.jsonDocument.root);
28+
const symbols = this.getSymbols(workflowDocument);
3029
return symbols;
3130
}
3231
return [];
3332
}
3433

35-
private getSymbols(document: TextDocument, root: ASTNode | undefined): DocumentSymbol[] {
34+
private getSymbols(workflowDocument: WorkflowDocument): DocumentSymbol[] {
35+
const root = workflowDocument.jsonDocument.root;
3636
if (!root) {
3737
return [];
3838
}
@@ -48,7 +48,7 @@ export class SymbolsProvider extends Provider {
4848
if (IGNORE_SYMBOL_NAMES.has(name)) {
4949
return;
5050
}
51-
const range = getRange(document, node);
51+
const range = workflowDocument.getNodeRange(node);
5252
const selectionRange = range;
5353
const symbol = { name, kind: this.getSymbolKind(node.type), range, selectionRange, children: [] };
5454
result.push(symbol);
@@ -70,8 +70,8 @@ export class SymbolsProvider extends Provider {
7070
if (IGNORE_SYMBOL_NAMES.has(name)) {
7171
return;
7272
}
73-
const range = getRange(document, property);
74-
const selectionRange = getRange(document, property.keyNode);
73+
const range = workflowDocument.getNodeRange(property);
74+
const selectionRange = workflowDocument.getNodeRange(property.keyNode);
7575
const children: DocumentSymbol[] = [];
7676
const symbol: DocumentSymbol = {
7777
name: name,

server/src/server.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
WorkspaceFolder,
88
} from "vscode-languageserver";
99
import { CleanWorkflowCommand } from "./commands/cleanWorkflow";
10-
import { WorkflowLanguageService, TextDocument } from "./languageTypes";
10+
import { WorkflowLanguageService, TextDocument, WorkflowDocument } from "./languageTypes";
1111
import { WorkflowDocuments } from "./models/workflowDocuments";
1212
import { SymbolsProvider } from "./providers/symbolsProvider";
1313
import { FormattingProvider } from "./providers/formattingProvider";
@@ -68,21 +68,34 @@ export class GalaxyWorkflowLanguageServer {
6868
this.documents.onDidClose((event) => this.onDidClose(event.document));
6969
}
7070

71-
private onDocumentOpen(document: TextDocument) {
72-
const workflowDocument = this.languageService.parseWorkflowDocument(document);
71+
private onDocumentOpen(textDocument: TextDocument) {
72+
const workflowDocument = this.languageService.parseWorkflowDocument(textDocument);
7373
this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument);
74+
this.validate(workflowDocument);
7475
}
7576

76-
private onDidChangeContent(document: TextDocument) {
77-
const workflowDocument = this.languageService.parseWorkflowDocument(document);
77+
private onDidChangeContent(textDocument: TextDocument) {
78+
const workflowDocument = this.languageService.parseWorkflowDocument(textDocument);
7879
this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument);
80+
this.validate(workflowDocument);
7981
}
8082

81-
private onDidClose(document: TextDocument) {
82-
this.workflowDocuments.removeWorkflowDocument(document.uri);
83+
private onDidClose(textDocument: TextDocument) {
84+
this.workflowDocuments.removeWorkflowDocument(textDocument.uri);
85+
this.clearValidation(textDocument);
8386
}
8487

8588
private cleanup() {
8689
this.workflowDocuments.dispose();
8790
}
91+
92+
private validate(workflowDocument: WorkflowDocument) {
93+
this.languageService.doValidation(workflowDocument).then((diagnostics) => {
94+
this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics });
95+
});
96+
}
97+
98+
private clearValidation(textDocument: TextDocument) {
99+
this.connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
100+
}
88101
}

server/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
],
88
"module": "commonjs",
99
"moduleResolution": "node",
10+
"resolveJsonModule": true,
11+
"esModuleInterop": true,
1012
"sourceMap": true,
1113
"strict": true
1214
},

0 commit comments

Comments
 (0)