Skip to content

Commit

Permalink
Add support for custom validation
Browse files Browse the repository at this point in the history
Implementing a collection of `ValidationContributor`s and adding them to
the language service through `setValidationContributors` will allow
to define custom validation rules.
  • Loading branch information
davelopez committed May 8, 2022
1 parent 06e91f7 commit 39d75fe
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 23 deletions.
30 changes: 30 additions & 0 deletions server/src/jsonUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ASTNode } from "./languageTypes";

export function getPathSegments(path: string): string[] | null {
const segments = path.split(/[/.]/);
// Skip leading `/` or `.`
if (!segments[0]) return segments.slice(1);
return segments;
}

export function getNodeFromPath(root: ASTNode, path: string): ASTNode | null {
let segments = getPathSegments(path);
if (!segments) return null;
if (segments.length === 1 && !segments[0]) return null;
let currentNode = root;
while (segments.length) {
const segment = segments[0];
segments = segments?.slice(1);
if (currentNode.type == "object") {
const property = currentNode.properties.find((p) => p.keyNode.value == segment);
if (property && !segments.length) return property;
if (!property?.valueNode) return null;
if (property.valueNode.type == "object") {
currentNode = property.valueNode;
} else {
return null;
}
}
}
return currentNode;
}
36 changes: 20 additions & 16 deletions server/src/languageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import { NativeWorkflowDocument } from "./models/nativeWorkflowDocument";
* A wrapper around the JSON Language Service to support language features
* for native Galaxy workflow files AKA '.ga' workflows.
*/
export class NativeWorkflowLanguageService implements WorkflowLanguageService {
export class NativeWorkflowLanguageService extends WorkflowLanguageService {
private _jsonLanguageService: LanguageService;
private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" };

constructor() {
super();
const params: LanguageServiceParams = {};
const settings = this.getLanguageSettings();
this._jsonLanguageService = getLanguageService(params);
Expand All @@ -41,27 +42,16 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService {
return NativeWorkflowSchema;
}

public parseWorkflowDocument(document: TextDocument): WorkflowDocument {
public override parseWorkflowDocument(document: TextDocument): WorkflowDocument {
const jsonDocument = this._jsonLanguageService.parseJSONDocument(document);
return new NativeWorkflowDocument(document, jsonDocument);
}

public format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] {
public override format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] {
return this._jsonLanguageService.format(document, range, options);
}

public async doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument;
const schemaValidationResults = await this._jsonLanguageService.doValidation(
nativeWorkflowDocument.textDocument,
nativeWorkflowDocument.jsonDocument,
this._documentSettings,
this.schema
);
return schemaValidationResults;
}

public async doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null> {
public override async doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null> {
const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument;
const hover = await this._jsonLanguageService.doHover(
nativeWorkflowDocument.textDocument,
Expand All @@ -71,7 +61,10 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService {
return hover;
}

public async doComplete(workflowDocument: WorkflowDocument, position: Position): Promise<CompletionList | null> {
public override async doComplete(
workflowDocument: WorkflowDocument,
position: Position
): Promise<CompletionList | null> {
const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument;
const completionResult = await this._jsonLanguageService.doComplete(
nativeWorkflowDocument.textDocument,
Expand All @@ -81,6 +74,17 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService {
return completionResult;
}

protected override async doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument;
const schemaValidationResults = await this._jsonLanguageService.doValidation(
nativeWorkflowDocument.textDocument,
nativeWorkflowDocument.jsonDocument,
this._documentSettings,
this.schema
);
return schemaValidationResults;
}

private getLanguageSettings(): LanguageSettings {
const settings: LanguageSettings = {
schemas: [this.getWorkflowSchemaConfig()],
Expand Down
38 changes: 32 additions & 6 deletions server/src/languageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,38 @@ export interface HoverContentContributor {
onHoverContent(workflowDocument: WorkflowDocument, position: Position): string;
}

export interface WorkflowLanguageService {
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
parseWorkflowDocument(document: TextDocument): WorkflowDocument;
doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]>;
doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null>;
doComplete(workflowDocument: WorkflowDocument, position: Position): Promise<CompletionList | null>;
/**
* Interface for contributing additional diagnostics to the validation process.
*/
export interface ValidationContributor {
/**
* Validates the given workflow document and provides diagnostics.
* @param workflowDocument The workflow document
*/
validate(workflowDocument: WorkflowDocument): Promise<Diagnostic[]>;
}

export abstract class WorkflowLanguageService {
protected _validationContributors: ValidationContributor[] = [];
public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
public abstract parseWorkflowDocument(document: TextDocument): WorkflowDocument;
public abstract doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null>;
public abstract doComplete(workflowDocument: WorkflowDocument, position: Position): Promise<CompletionList | null>;

protected abstract doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]>;

public setValidationContributors(contributors: ValidationContributor[]): void {
this._validationContributors = contributors;
}

public async validate(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
const diagnostics = await this.doValidation(workflowDocument);
this._validationContributors.forEach(async (contributor) => {
const contributedDiagnostics = await contributor.validate(workflowDocument);
diagnostics.push(...contributedDiagnostics);
});
return diagnostics;
}
}

export abstract class ServerContext {
Expand Down
7 changes: 7 additions & 0 deletions server/src/models/nativeWorkflowDocument.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JSONDocument } from "vscode-json-languageservice";
import { getNodeFromPath } from "../jsonUtils";
import { TextDocument, Range, Position, ASTNode, WorkflowDocument } from "../languageTypes";

/**
Expand Down Expand Up @@ -65,4 +66,10 @@ export class NativeWorkflowDocument extends WorkflowDocument {
}
return parent.children[previousNodeIndex];
}

public override getNodeFromPath(path: string): ASTNode | null {
const root = this._jsonDocument.root;
if (!root) return null;
return getNodeFromPath(root, path);
}
}
7 changes: 7 additions & 0 deletions server/src/models/workflowDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export abstract class WorkflowDocument {

public abstract getDocumentRange(): Range;

public abstract getNodeFromPath(path: string): ASTNode | null;

/** Returns a small Range at the beginning of the document */
public getDefaultRange(): Range {
return Range.create(this.textDocument.positionAt(0), this.textDocument.positionAt(1));
}

public getNodeRange(node: ASTNode): Range {
return Range.create(
this.textDocument.positionAt(node.offset),
Expand Down
2 changes: 1 addition & 1 deletion server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class GalaxyWorkflowLanguageServer {
if (WorkflowDocuments.schemesToSkip.includes(workflowDocument.uri.scheme)) {
return;
}
this.languageService.doValidation(workflowDocument).then((diagnostics) => {
this.languageService.validate(workflowDocument).then((diagnostics) => {
this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics });
});
}
Expand Down

0 comments on commit 39d75fe

Please sign in to comment.