Skip to content

Commit

Permalink
Added in parser settings and allow server to distinguish between kube…
Browse files Browse the repository at this point in the history
…rnetes and not
  • Loading branch information
JPinkney committed Feb 8, 2018
1 parent 19e7273 commit 014655a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 76 deletions.
125 changes: 56 additions & 69 deletions src/languageService/parser/jsonParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { JSONSchema } from '../jsonSchema';
import * as objects from '../utils/objects';

import * as nls from 'vscode-nls';
import { LanguageSettings } from '../yamlLanguageService';
const localize = nls.loadMessageBundle();

export interface IRange {
Expand Down Expand Up @@ -45,14 +46,12 @@ export interface IProblem {
message: string;
}

let isKubernetes = false;

export class ASTNode {
public start: number;
public end: number;
public type: string;
public parent: ASTNode;

public parserSettings: LanguageSettings;
public location: Json.Segment;

constructor(parent: ASTNode, type: string, location: Json.Segment, start: number, end?: number) {
Expand All @@ -61,6 +60,13 @@ export class ASTNode {
this.start = start;
this.end = end;
this.parent = parent;
this.parserSettings = {
isKubernetes: false
};
}

public setParserSettings(parserSettings: LanguageSettings){
this.parserSettings = parserSettings;
}

public getPath(): Json.JSONPath {
Expand Down Expand Up @@ -219,27 +225,14 @@ export class ASTNode {
}
if (!bestMatch) {
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
}else {
if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
// no errors, both are equally good matches
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
} else {
let compareResult = subValidationResult.compare(bestMatch.validationResult);
if (compareResult > 0) {
// our node is the best matching so far
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
} else if (compareResult === 0) {
// there's already a best matching but we are as good
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.mergeEnumValues(subValidationResult);
}
}
} else if(this.parserSettings.isKubernetes) {
bestMatch = alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas);
} else {
bestMatch = genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas);
}
});

if (matches.length > 1 && maxOneMatch && !isKubernetes) {
if (matches.length > 1 && maxOneMatch && !this.parserSettings.isKubernetes) {
validationResult.problems.push({
location: { start: this.start, end: this.start + 1 },
severity: ProblemSeverity.Warning,
Expand Down Expand Up @@ -789,7 +782,7 @@ export class ObjectASTNode extends ASTNode {
}
});
}
}
}

if (schema.maxProperties) {
if (this.properties.length > schema.maxProperties) {
Expand Down Expand Up @@ -836,49 +829,9 @@ export class ObjectASTNode extends ASTNode {
}
});
}

//Add the x-kubernetes-group-version-kind to the enum of apiVersion/kind for autocompletion and validation
if (schema["x-kubernetes-group-version-kind"]) {
isKubernetes = true;
Object.keys(schema.properties).forEach((propertyName: string) => {
let child = seenKeys[propertyName];
if (child && propertyName == "apiVersion") {
if(!schema.properties[propertyName].enum){
schema.properties[propertyName].enum = [];
let enumSet = new Set();
schema["x-kubernetes-group-version-kind"].forEach(customObj => {
if(!enumSet.has(customObj.Version)){
schema.properties[propertyName].enum.push(customObj.Version);
enumSet.add(customObj.Version);
}
});
}

}

if (child && propertyName == "kind") {
if(!schema.properties[propertyName].enum){
schema.properties[propertyName].enum = [];
let enumSet = new Set();
schema["x-kubernetes-group-version-kind"].forEach(customObj => {
if(!enumSet.has(customObj.Kind)){
schema.properties[propertyName].enum.push(customObj.Kind);
enumSet.add(customObj.Kind);
}
});
}
}
});
}

}
}

export interface JSONDocumentConfig {
disallowComments?: boolean;
isKubernetes?: boolean;
}

export interface IApplicableSchema {
node: ASTNode;
inverted?: boolean;
Expand Down Expand Up @@ -981,13 +934,6 @@ export class ValidationResult {
}
}

public compare(other: ValidationResult): number {
if(isKubernetes){
return this.compareKubernetes(other);
}
return this.compareGeneric(other);
}

public compareGeneric(other: ValidationResult): number {
let hasProblems = this.hasProblems();
if (hasProblems !== other.hasProblems()) {
Expand Down Expand Up @@ -1046,6 +992,12 @@ export class JSONDocument {
}
}

public configureSettings(parserSettings: LanguageSettings){
if(this.root) {
this.root.setParserSettings(parserSettings);
}
}

public validate(schema: JSONSchema): IProblem[] {
if (this.root && schema) {
let validationResult = new ValidationResult();
Expand Down Expand Up @@ -1073,3 +1025,38 @@ export class JSONDocument {
return validationResult.problems;
}
}

//Alternative comparison is specifically used by the kubernetes/openshift schema but may lead to better results then genericComparison depending on the schema
function alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas){
let compareResult = subValidationResult.compareKubernetes(bestMatch.validationResult);
if (compareResult > 0) {
// our node is the best matching so far
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
} else if (compareResult === 0) {
// there's already a best matching but we are as good
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.mergeEnumValues(subValidationResult);
}
return bestMatch;
}

//genericComparison tries to find the best matching schema using a generic comparison
function genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas){
if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
// no errors, both are equally good matches
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
} else {
let compareResult = subValidationResult.compareGeneric(bestMatch.validationResult);
if (compareResult > 0) {
// our node is the best matching so far
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
} else if (compareResult === 0) {
// there's already a best matching but we are as good
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.mergeEnumValues(subValidationResult);
}
}
return bestMatch;
}
7 changes: 6 additions & 1 deletion src/languageService/yamlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { JSONDocument } from 'vscode-json-languageservice';
import { YAMLHover } from "./services/yamlHover";
import { YAMLValidation } from "./services/yamlValidation";
import { YAMLDocument, Diagnostic } from 'vscode-yaml-languageservice';
//const jsonValidation_1 = require("vscode-json-languageservice/lib/services/jsonValidation");

export interface LanguageSettings {
validate?: boolean; //Setting for whether we want to validate the schema
isKubernetes?: boolean; //If true then its validating against kubernetes
schemas?: any[]; //List of schemas
}

export interface PromiseConstructor {
/**
Expand Down
31 changes: 27 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import fs = require('fs');
import URI from './languageService/utils/uri';
import * as URL from 'url';
import Strings = require('./languageService/utils/strings');
import { YAMLDocument, JSONSchema, LanguageSettings, getLanguageService } from 'vscode-yaml-languageservice';
import { YAMLDocument, JSONSchema, getLanguageService } from 'vscode-yaml-languageservice';
import { getLineOffsets, removeDuplicatesObj } from './languageService/utils/arrUtils';
import { getLanguageService as getCustomLanguageService } from './languageService/yamlLanguageService';
import { getLanguageService as getCustomLanguageService, LanguageSettings } from './languageService/yamlLanguageService';
import * as nls from 'vscode-nls';
import { FilePatternAssociation } from './languageService/services/jsonSchemaService';
import { parse as parseYAML } from './languageService/parser/yamlParser';
import { JSONDocument } from './languageService/parser/jsonParser';
nls.config(process.env['VSCODE_NLS_CONFIG']);

interface ISchemaAssociations {
Expand Down Expand Up @@ -171,7 +172,7 @@ export let languageService = getLanguageService({
contributions: []
});

export let KUBERNETES_SCHEMA_URL = "https://raw.githubusercontent.com/garethr/openshift-json-schema/master/v3.6.0-standalone-strict/all.json";
export let KUBERNETES_SCHEMA_URL = "https://gist.githubusercontent.com/JPinkney/ccaf3909ef811e5657ca2e2e1fa05d76/raw/f85e51bfb67fdb99ab7653c2953b60087cc871ea/openshift_schema_all.json";
export let KEDGE_SCHEMA_URL = "https://raw.githubusercontent.com/kedgeproject/json-schema/master/master/kedge-json-schema.json";
export let customLanguageService = getCustomLanguageService(schemaRequestService, workspaceContext, [],
(resource) => connection.sendRequest(CustomSchemaRequest.type, resource));
Expand Down Expand Up @@ -359,6 +360,25 @@ function configureSchemas(uri, fileMatch, schema, languageSettings){
return languageSettings;
}

function setKubernetesParserOption(jsonDocuments: JSONDocument[], option: boolean){
for(let jsonDoc in jsonDocuments){
jsonDocuments[jsonDoc].configureSettings({
isKubernetes: option
});
}
}

function isKubernetes(textDocument){
for(let path in specificValidatorPaths){
let globPath = specificValidatorPaths[path];
let fpa = new FilePatternAssociation(globPath);
if(fpa.matchesPattern(textDocument.uri)){
return true;
}
}
return false;
}

documents.onDidChangeContent((change) => {
triggerValidation(change.document);
});
Expand Down Expand Up @@ -393,8 +413,9 @@ function validateTextDocument(textDocument: TextDocument): void {
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
return;
}

let yamlDocument = parseYAML(textDocument.getText());
isKubernetes(textDocument) ? setKubernetesParserOption(yamlDocument.documents, true) : setKubernetesParserOption(yamlDocument.documents, false);
customLanguageService.doValidation(textDocument, yamlDocument).then(function(diagnosticResults){

let diagnostics = [];
Expand Down Expand Up @@ -425,6 +446,7 @@ connection.onCompletion(textDocumentPosition => {
let completionFix = completionHelper(textDocument, textDocumentPosition.position);
let newText = completionFix.newText;
let jsonDocument = parseYAML(newText);
isKubernetes(textDocument) ? setKubernetesParserOption(jsonDocument.documents, true) : setKubernetesParserOption(jsonDocument.documents, false);
return customLanguageService.doComplete(textDocument, textDocumentPosition.position, jsonDocument);
});

Expand Down Expand Up @@ -492,6 +514,7 @@ connection.onCompletionResolve(completionItem => {
connection.onHover(textDocumentPositionParams => {
let document = documents.get(textDocumentPositionParams.textDocument.uri);
let jsonDocument = parseYAML(document.getText());
isKubernetes(document) ? setKubernetesParserOption(jsonDocument.documents, true) : setKubernetesParserOption(jsonDocument.documents, false);
return customLanguageService.doHover(document, textDocumentPositionParams.position, jsonDocument);
});

Expand Down
14 changes: 12 additions & 2 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let languageService = getLanguageService(schemaRequestService, workspaceContext,

let schemaService = new JSONSchemaService(schemaRequestService, workspaceContext);

let uri = "https://gist.githubusercontent.com/JPinkney/c32f25af8e8d7322b38f8c7864fabcfe/raw/6bd43cb27f4e665a8529b2289b42374ae3267195/openshift_schema.json";
let uri = "https://gist.githubusercontent.com/JPinkney/ccaf3909ef811e5657ca2e2e1fa05d76/raw/f85e51bfb67fdb99ab7653c2953b60087cc871ea/openshift_schema_all.json";
let languageSettings = {
schemas: [],
validate: true
Expand All @@ -48,6 +48,11 @@ suite("Kubernetes Integration Tests", () => {
function parseSetup(content: string){
let testTextDocument = setup(content);
let yDoc = parseYAML(testTextDocument.getText());
for(let jsonDoc in yDoc.documents){
yDoc.documents[jsonDoc].configureSettings({
isKubernetes: true
});
}
return languageService.doValidation(testTextDocument, yDoc);
}

Expand Down Expand Up @@ -121,7 +126,7 @@ suite("Kubernetes Integration Tests", () => {
});

it('Type Array does not error on valid node', (done) => {
let content = `items:\n - test: test`;
let content = `items:\n - apiVersion: v1`;
let validator = parseSetup(content);
validator.then(function(result){
assert.equal(result.length, 0);
Expand Down Expand Up @@ -196,6 +201,11 @@ suite("Kubernetes Integration Tests", () => {
function parseSetup(content: string, position){
let testTextDocument = setup(content);
let yDoc = parseYAML(testTextDocument.getText());
for(let jsonDoc in yDoc.documents){
yDoc.documents[jsonDoc].configureSettings({
isKubernetes: true
});
}
return completionHelper(testTextDocument, testTextDocument.positionAt(position));
}

Expand Down

0 comments on commit 014655a

Please sign in to comment.