Skip to content

Commit 38da500

Browse files
andxuJPinkney
authored andcommitted
handle scenario of multiple documents in single yaml file (#81)
* add custom schemaSequence in schema object to map schema to multiple documents in single yaml file
1 parent 9b80bcc commit 38da500

File tree

7 files changed

+169
-15
lines changed

7 files changed

+169
-15
lines changed

src/languageservice/jsonSchema.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface JSONSchema {
4444
patternErrorMessage?: string; // VSCode extension
4545
deprecationMessage?: string; // VSCode extension
4646
enumDescriptions?: string[]; // VSCode extension
47+
schemaSequence?: JSONSchema[]; // extension for multiple schemas related to multiple documents in single yaml file
4748
"x-kubernetes-group-version-kind"?; //Kubernetes extension
4849
}
4950

src/languageservice/services/jsonSchemaService.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ export class JSONSchemaService implements IJSONSchemaService {
501501
}
502502
collectEntries(next.items, next.additionalProperties, next.not);
503503
collectMapEntries(next.definitions, next.properties, next.patternProperties, <JSONSchemaMap>next.dependencies);
504-
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, <JSONSchema[]>next.items);
504+
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, <JSONSchema[]>next.items, next.schemaSequence);
505505
}
506506
return this.promise.all(openPromises);
507507
};

src/languageservice/services/yamlCompletion.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class YAMLCompletion {
5050
return this.promise.resolve(item);
5151
}
5252

53-
public doComplete(document: TextDocument, position: Position, doc: Parser.JSONDocument): Thenable<CompletionList> {
53+
public doComplete(document: TextDocument, position: Position, doc): Thenable<CompletionList> {
5454

5555
let result: CompletionList = {
5656
items: [],
@@ -66,6 +66,7 @@ export class YAMLCompletion {
6666
if(currentDoc === null){
6767
return Promise.resolve(result);
6868
}
69+
const currentDocIndex = doc.documents.indexOf(currentDoc);
6970
let node = currentDoc.getNodeFromOffsetEndInclusive(offset);
7071
if (this.isInComment(document, node ? node.start : 0, offset)) {
7172
return Promise.resolve(result);
@@ -123,6 +124,10 @@ export class YAMLCompletion {
123124
if(!schema){
124125
return Promise.resolve(result);
125126
}
127+
let newSchema = schema;
128+
if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[currentDocIndex]) {
129+
newSchema = new SchemaService.ResolvedSchema(schema.schema.schemaSequence[currentDocIndex]);
130+
}
126131

127132
let collectionPromises: Thenable<any>[] = [];
128133

@@ -160,9 +165,9 @@ export class YAMLCompletion {
160165
separatorAfter = this.evaluateSeparatorAfter(document, document.offsetAt(overwriteRange.end));
161166
}
162167

163-
if (schema) {
168+
if (newSchema) {
164169
// property proposals with schema
165-
this.getPropertyCompletions(schema, currentDoc, node, addValue, collector, separatorAfter);
170+
this.getPropertyCompletions(newSchema, currentDoc, node, addValue, collector, separatorAfter);
166171
}
167172

168173
let location = node.getPath();
@@ -185,8 +190,8 @@ export class YAMLCompletion {
185190

186191
// proposals for values
187192
let types: { [type: string]: boolean } = {};
188-
if (schema) {
189-
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types);
193+
if (newSchema) {
194+
this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector, types);
190195
}
191196
if (this.contributions.length > 0) {
192197
this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises);

src/languageservice/services/yamlHover.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class YAMLHover {
2626
this.promise = promiseConstructor || Promise;
2727
}
2828

29-
public doHover(document: TextDocument, position: Position, doc: Parser.JSONDocument): Thenable<Hover> {
29+
public doHover(document: TextDocument, position: Position, doc): Thenable<Hover> {
3030

3131
if(!document){
3232
this.promise.resolve(void 0);
@@ -37,6 +37,7 @@ export class YAMLHover {
3737
if(currentDoc === null){
3838
return this.promise.resolve(void 0);
3939
}
40+
const currentDocIndex = doc.documents.indexOf(currentDoc);
4041
let node = currentDoc.getNodeFromOffset(offset);
4142
if (!node || (node.type === 'object' || node.type === 'array') && offset > node.start + 1 && offset < node.end - 1) {
4243
return this.promise.resolve(void 0);
@@ -76,8 +77,11 @@ export class YAMLHover {
7677

7778
return this.schemaService.getSchemaForResource(document.uri).then((schema) => {
7879
if (schema) {
79-
80-
let matchingSchemas = currentDoc.getMatchingSchemas(schema.schema, node.start);
80+
let newSchema = schema;
81+
if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[currentDocIndex]) {
82+
newSchema = new SchemaService.ResolvedSchema(schema.schema.schemaSequence[currentDocIndex]);
83+
}
84+
let matchingSchemas = currentDoc.getMatchingSchemas(newSchema.schema, node.start);
8185

8286
let title: string = null;
8387
let markdownDescription: string = null;

src/languageservice/services/yamlValidation.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*--------------------------------------------------------------------------------------------*/
66
'use strict';
77

8-
import { JSONSchemaService } from './jsonSchemaService';
8+
import { JSONSchemaService, ResolvedSchema } from './jsonSchemaService';
99
import { JSONDocument, ObjectASTNode, IProblem, ProblemSeverity } from '../parser/jsonParser';
1010
import { TextDocument, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types';
1111
import { PromiseConstructor, Thenable, LanguageSettings} from '../yamlLanguageService';
@@ -38,22 +38,26 @@ export class YAMLValidation {
3838
return this.jsonSchemaService.getSchemaForResource(textDocument.uri).then(function (schema) {
3939
var diagnostics = [];
4040
var added = {};
41-
41+
let newSchema = schema;
4242
if (schema) {
43-
43+
let documentIndex = 0;
4444
for(let currentYAMLDoc in yamlDocument.documents){
4545
let currentDoc = yamlDocument.documents[currentYAMLDoc];
46-
let diagnostics = currentDoc.getValidationProblems(schema.schema);
46+
if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[documentIndex]) {
47+
newSchema = new ResolvedSchema(schema.schema.schemaSequence[documentIndex]);
48+
}
49+
let diagnostics = currentDoc.getValidationProblems(newSchema.schema);
4750
for(let diag in diagnostics){
4851
let curDiagnostic = diagnostics[diag];
4952
currentDoc.errors.push({ location: { start: curDiagnostic.location.start, end: curDiagnostic.location.end }, message: curDiagnostic.message })
5053
}
54+
documentIndex++;
5155
}
5256

5357
}
54-
if(schema && schema.errors.length > 0){
58+
if(newSchema && newSchema.errors.length > 0){
5559

56-
for(let curDiagnostic of schema.errors){
60+
for(let curDiagnostic of newSchema.errors){
5761
diagnostics.push({
5862
severity: DiagnosticSeverity.Error,
5963
range: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"schemaSequence": [
3+
{
4+
"$schema": "http://json-schema.org/draft-04/schema#",
5+
"description": "Person object",
6+
"type": "object",
7+
"properties": {
8+
"name": {
9+
"type": "string",
10+
"description": "The name of this person"
11+
},
12+
"age": {
13+
"type": "number",
14+
"description": "The age of this person"
15+
}
16+
},
17+
"required":["name","age"]
18+
},
19+
{
20+
"$ref": "http://json.schemastore.org/bowerrc"
21+
}
22+
]
23+
24+
}

test/mulipleDocuments.test.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { TextDocument } from 'vscode-languageserver';
6+
import {getLanguageService} from '../src/languageservice/yamlLanguageService'
7+
import path = require('path');
8+
import {schemaRequestService, workspaceContext} from './testHelper';
9+
import { parse as parseYAML } from '../src/languageservice/parser/yamlParser';
10+
var assert = require('assert');
11+
12+
let languageService = getLanguageService(schemaRequestService, workspaceContext, [], null);
13+
14+
function toFsPath(str): string {
15+
if (typeof str !== 'string') {
16+
throw new TypeError(`Expected a string, got ${typeof str}`);
17+
}
18+
19+
let pathName;
20+
pathName = path.resolve(str);
21+
pathName = pathName.replace(/\\/g, '/');
22+
// Windows drive letter must be prefixed with a slash
23+
if (pathName[0] !== '/') {
24+
pathName = `/${pathName}`;
25+
}
26+
return encodeURI(`file://${pathName}`).replace(/[?#]/g, encodeURIComponent);
27+
}
28+
29+
let uri = toFsPath(path.join(__dirname, './fixtures/customMultipleSchemaSequences.json'));
30+
let languageSettings = {
31+
schemas: [],
32+
validate: true,
33+
customTags: []
34+
};
35+
let fileMatch = ["*.yml", "*.yaml"];
36+
languageSettings.schemas.push({ uri, fileMatch: fileMatch });
37+
languageSettings.customTags.push("!Test");
38+
languageSettings.customTags.push("!Ref sequence");
39+
languageService.configure(languageSettings);
40+
// Defines a Mocha test suite to group tests of similar kind together
41+
suite("Multiple Documents Validation Tests", () => {
42+
43+
// Tests for validator
44+
describe('Multiple Documents Validation', function() {
45+
function setup(content: string){
46+
return TextDocument.create("file://~/Desktop/vscode-k8s/test.yaml", "yaml", 0, content);
47+
}
48+
49+
function validatorSetup(content: string){
50+
const testTextDocument = setup(content);
51+
const yDoc = parseYAML(testTextDocument.getText(), languageSettings.customTags);
52+
return languageService.doValidation(testTextDocument, yDoc);
53+
}
54+
55+
function hoverSetup(content: string, position){
56+
let testTextDocument = setup(content);
57+
let jsonDocument = parseYAML(testTextDocument.getText());
58+
return languageService.doHover(testTextDocument, testTextDocument.positionAt(position), jsonDocument);
59+
}
60+
61+
it('Should validate multiple documents', (done) => {
62+
const content = `
63+
name: jack
64+
age: 22
65+
---
66+
analytics: true
67+
`;
68+
const validator = validatorSetup(content);
69+
validator.then((result) => {
70+
assert.equal(result.length, 0);
71+
}).then(done, done);
72+
});
73+
74+
it('Should find errors in both documents', (done) => {
75+
let content = `name1: jack
76+
age: asd
77+
---
78+
cwd: False`;
79+
let validator = validatorSetup(content);
80+
validator.then(function(result){
81+
assert.equal(result.length, 3);
82+
}).then(done, done);
83+
});
84+
85+
it('Should find errors in first document', (done) => {
86+
let content = `name: jack
87+
age: age
88+
---
89+
analytics: true`;
90+
let validator = validatorSetup(content);
91+
validator.then(function(result){
92+
assert.equal(result.length, 1);
93+
}).then(done, done);
94+
});
95+
96+
it('Should find errors in second document', (done) => {
97+
let content = `name: jack
98+
age: 22
99+
---
100+
cwd: False`;
101+
let validator = validatorSetup(content);
102+
validator.then(function(result){
103+
assert.equal(result.length, 1);
104+
}).then(done, done);
105+
});
106+
107+
it('Should hover in first document', (done) => {
108+
let content = `name: jack\nage: 22\n---\ncwd: False`;
109+
let hover = hoverSetup(content, 1 + content.indexOf('age'));
110+
hover.then(function(result){
111+
assert.notEqual(result.contents.length, 0);
112+
assert.equal(result.contents[0], 'The age of this person');
113+
}).then(done, done);
114+
});
115+
});
116+
});

0 commit comments

Comments
 (0)