Skip to content

Commit

Permalink
Merge pull request #18 from davelopez/explore_schema_validation
Browse files Browse the repository at this point in the history
Add basic JSON schema validation
  • Loading branch information
davelopez authored Apr 4, 2022
2 parents 49b5431 + 5ef6ece commit 66f756a
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 31 deletions.
60 changes: 55 additions & 5 deletions server/src/languageService.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { ASTNode, getLanguageService, LanguageService } from "vscode-json-languageservice";
import {
getLanguageService,
LanguageService,
LanguageServiceParams,
DocumentLanguageSettings,
Diagnostic,
JSONSchema,
LanguageSettings,
SchemaConfiguration,
} from "vscode-json-languageservice";
import {
TextDocument,
Range,
FormattingOptions,
TextEdit,
WorkflowDocument,
WorkflowLanguageService,
Position,
Hover,
} from "./languageTypes";
import NativeWorkflowSchema from "../../workflow-languages/schemas/native.schema.json";

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

constructor() {
this._jsonLanguageService = getLanguageService({});
const params: LanguageServiceParams = {};
const settings = this.getLanguageSettings();
this._jsonLanguageService = getLanguageService(params);
this._jsonLanguageService.configure(settings);
}

public get schema(): JSONSchema {
return NativeWorkflowSchema;
}

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

export function getRange(document: TextDocument, node: ASTNode) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
public async doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
const schemaValidationResults = await this._jsonLanguageService.doValidation(
workflowDocument.textDocument,
workflowDocument.jsonDocument,
this._documentSettings,
this.schema
);
return schemaValidationResults;
}

public async doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null> {
const hover = await this._jsonLanguageService.doHover(
workflowDocument.textDocument,
position,
workflowDocument.jsonDocument
);
return hover;
}

private getLanguageSettings(): LanguageSettings {
const settings: LanguageSettings = {
schemas: [this.getWorkflowSchemaConfig()],
};
return settings;
}

private getWorkflowSchemaConfig(): SchemaConfiguration {
return {
uri: this.schema.id ?? "",
fileMatch: ["**.ga"],
schema: this.schema,
};
}
}
1 change: 1 addition & 0 deletions server/src/languageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface FormattingOptions extends LSPFormattingOptions {
export interface WorkflowLanguageService {
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
parseWorkflowDocument(document: TextDocument): WorkflowDocument;
doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]>;
}

export abstract class ServerContext {
Expand Down
24 changes: 23 additions & 1 deletion server/src/models/workflowDocument.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JSONDocument } from "vscode-json-languageservice";
import { TextDocument } from "../languageTypes";
import { TextDocument, Range, Position, ASTNode } from "../languageTypes";

/**
* This class contains information about workflow semantics.
Expand All @@ -25,4 +25,26 @@ export class WorkflowDocument {
public get jsonDocument(): JSONDocument {
return this._jsonDocument;
}

public getNodeAtPosition(position: Position): ASTNode | undefined {
const offset = this.textDocument.offsetAt(position);
return this.jsonDocument.getNodeFromOffset(offset);
}

public getNodeRange(node: ASTNode): Range {
return Range.create(
this.textDocument.positionAt(node.offset),
this.textDocument.positionAt(node.offset + node.length)
);
}

public getNodeRangeAtPosition(position: Position): Range {
const node = this.getNodeAtPosition(position);
return node ? this.getNodeRange(node) : this.getDefaultRangeAtPosition(position);
}

private getDefaultRangeAtPosition(position: Position): Range {
const offset = this.textDocument.offsetAt(position);
return Range.create(this.textDocument.positionAt(offset), this.textDocument.positionAt(offset + 1));
}
}
14 changes: 3 additions & 11 deletions server/src/providers/hoverProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Hover, HoverParams, MarkupKind, Position, ASTNode, WorkflowDocument, PropertyASTNode } from "../languageTypes";
import { Hover, HoverParams, MarkupKind, ASTNode, PropertyASTNode } from "../languageTypes";
import { GalaxyWorkflowLanguageServer } from "../server";
import { Provider } from "./provider";
import { getRange } from "../languageService";
import { ArrayASTNode, BooleanASTNode, NullASTNode, NumberASTNode, StringASTNode } from "vscode-json-languageservice";

export class HoverProvider extends Provider {
Expand All @@ -23,12 +22,12 @@ export class HoverProvider extends Provider {
if (!workflowDocument) {
return undefined;
}
const node = this.getNodeAtDocumentPosition(workflowDocument, params.position);
const node = workflowDocument.getNodeAtPosition(params.position);
if (!node) {
return undefined;
}
const contentLines = this.printNode(node);
const hoverRange = getRange(workflowDocument.textDocument, node);
const hoverRange = workflowDocument.getNodeRange(node);
const markdown = {
kind: MarkupKind.Markdown,
value: contentLines.join("\n\n"),
Expand All @@ -40,13 +39,6 @@ export class HoverProvider extends Provider {
return result;
}

private getNodeAtDocumentPosition(workflowDocument: WorkflowDocument, position: Position): ASTNode | undefined {
const document = workflowDocument.textDocument;
const offset = document.offsetAt(position);
const node = workflowDocument.jsonDocument.getNodeFromOffset(offset);
return node;
}

private printNode(node: ASTNode): string[] {
const contentLines = [`## ${node.type}`];
if (node.type === "object") {
Expand Down
14 changes: 7 additions & 7 deletions server/src/providers/symbolsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { getRange } from "../languageService";
import {
TextDocument,
DocumentSymbolParams,
DocumentSymbol,
SymbolKind,
ASTNode,
PropertyASTNode,
ObjectASTNode,
WorkflowDocument,
} from "../languageTypes";
import { GalaxyWorkflowLanguageServer } from "../server";
import { Provider } from "./provider";
Expand All @@ -26,13 +25,14 @@ export class SymbolsProvider extends Provider {
public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] {
const workflowDocument = this.workflowDocuments.get(params.textDocument.uri);
if (workflowDocument) {
const symbols = this.getSymbols(workflowDocument.textDocument, workflowDocument.jsonDocument.root);
const symbols = this.getSymbols(workflowDocument);
return symbols;
}
return [];
}

private getSymbols(document: TextDocument, root: ASTNode | undefined): DocumentSymbol[] {
private getSymbols(workflowDocument: WorkflowDocument): DocumentSymbol[] {
const root = workflowDocument.jsonDocument.root;
if (!root) {
return [];
}
Expand All @@ -48,7 +48,7 @@ export class SymbolsProvider extends Provider {
if (IGNORE_SYMBOL_NAMES.has(name)) {
return;
}
const range = getRange(document, node);
const range = workflowDocument.getNodeRange(node);
const selectionRange = range;
const symbol = { name, kind: this.getSymbolKind(node.type), range, selectionRange, children: [] };
result.push(symbol);
Expand All @@ -70,8 +70,8 @@ export class SymbolsProvider extends Provider {
if (IGNORE_SYMBOL_NAMES.has(name)) {
return;
}
const range = getRange(document, property);
const selectionRange = getRange(document, property.keyNode);
const range = workflowDocument.getNodeRange(property);
const selectionRange = workflowDocument.getNodeRange(property.keyNode);
const children: DocumentSymbol[] = [];
const symbol: DocumentSymbol = {
name: name,
Expand Down
27 changes: 20 additions & 7 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
WorkspaceFolder,
} from "vscode-languageserver";
import { CleanWorkflowCommand } from "./commands/cleanWorkflow";
import { WorkflowLanguageService, TextDocument } from "./languageTypes";
import { WorkflowLanguageService, TextDocument, WorkflowDocument } from "./languageTypes";
import { WorkflowDocuments } from "./models/workflowDocuments";
import { SymbolsProvider } from "./providers/symbolsProvider";
import { FormattingProvider } from "./providers/formattingProvider";
Expand Down Expand Up @@ -68,21 +68,34 @@ export class GalaxyWorkflowLanguageServer {
this.documents.onDidClose((event) => this.onDidClose(event.document));
}

private onDocumentOpen(document: TextDocument) {
const workflowDocument = this.languageService.parseWorkflowDocument(document);
private onDocumentOpen(textDocument: TextDocument) {
const workflowDocument = this.languageService.parseWorkflowDocument(textDocument);
this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument);
this.validate(workflowDocument);
}

private onDidChangeContent(document: TextDocument) {
const workflowDocument = this.languageService.parseWorkflowDocument(document);
private onDidChangeContent(textDocument: TextDocument) {
const workflowDocument = this.languageService.parseWorkflowDocument(textDocument);
this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument);
this.validate(workflowDocument);
}

private onDidClose(document: TextDocument) {
this.workflowDocuments.removeWorkflowDocument(document.uri);
private onDidClose(textDocument: TextDocument) {
this.workflowDocuments.removeWorkflowDocument(textDocument.uri);
this.clearValidation(textDocument);
}

private cleanup() {
this.workflowDocuments.dispose();
}

private validate(workflowDocument: WorkflowDocument) {
this.languageService.doValidation(workflowDocument).then((diagnostics) => {
this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics });
});
}

private clearValidation(textDocument: TextDocument) {
this.connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
}
}
2 changes: 2 additions & 0 deletions server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
],
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true,
"strict": true
},
Expand Down
Loading

0 comments on commit 66f756a

Please sign in to comment.