Skip to content

Commit 8f0da51

Browse files
gorkemJPinkney
authored andcommitted
Add in API for adding/deleting schemas and modifying their contents in memory"
1 parent a66f06d commit 8f0da51

5 files changed

Lines changed: 297 additions & 5 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { JSONSchemaService } from '../services/jsonSchemaService';
6+
import { JSONSchema } from '../jsonSchema04';
7+
8+
export enum MODIFICATION_ACTIONS {
9+
'delete',
10+
'add'
11+
}
12+
13+
export interface SchemaAdditions {
14+
schema: string,
15+
action: MODIFICATION_ACTIONS.add,
16+
path: string,
17+
key: string,
18+
// tslint:disable-next-line: no-any
19+
content: any
20+
}
21+
22+
export interface SchemaDeletions {
23+
schema: string,
24+
action: MODIFICATION_ACTIONS.delete,
25+
path: string,
26+
key: string
27+
}
28+
29+
export class SchemaModification {
30+
31+
/**
32+
* Add content to a specified schema at a specified path
33+
*/
34+
public async addContent(schemaService: JSONSchemaService, additions: SchemaAdditions) {
35+
const schema = await schemaService.getResolvedSchema(additions.schema);
36+
if (schema) {
37+
const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, additions.path);
38+
39+
if (typeof resolvedSchemaLocation === 'object') {
40+
resolvedSchemaLocation[additions.key] = additions.content;
41+
}
42+
await schemaService.saveSchema(additions.schema, schema.schema);
43+
}
44+
}
45+
46+
/**
47+
* Delete content in a specified schema at a specified path
48+
*/
49+
public async deleteContent(schemaService: JSONSchemaService, deletions: SchemaDeletions) {
50+
const schema = await schemaService.getResolvedSchema(deletions.schema);
51+
if (schema) {
52+
const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, deletions.path);
53+
54+
if (typeof resolvedSchemaLocation === 'object') {
55+
delete resolvedSchemaLocation[deletions.key];
56+
}
57+
await schemaService.saveSchema(deletions.schema, schema.schema);
58+
}
59+
}
60+
61+
/**
62+
* Take a JSON Schema and the path that you would like to get to
63+
* @returns the JSON Schema resolved at that specific path
64+
*/
65+
private resolveJSONSchemaToSection(schema: JSONSchema, paths: string): JSONSchema {
66+
const splitPathway = paths.split('/');
67+
let resolvedSchemaLocation = schema;
68+
for (const path of splitPathway) {
69+
if (path === '') {
70+
continue;
71+
}
72+
this.resolveNext(resolvedSchemaLocation, path);
73+
resolvedSchemaLocation = resolvedSchemaLocation[path];
74+
}
75+
return resolvedSchemaLocation;
76+
}
77+
78+
/**
79+
* Resolve the next Object if they have compatible types
80+
* @param object a location in the JSON Schema
81+
* @param token the next token that you want to search for
82+
*/
83+
// tslint:disable-next-line: no-any
84+
private resolveNext(object: any, token: any) {
85+
// tslint:disable-next-line: no-any
86+
if (Array.isArray(object) && isNaN(token)) {
87+
throw new Error('Expected a number after the array object');
88+
} else if (typeof object === 'object' && typeof token !== 'string') {
89+
throw new Error('Expected a string after the object');
90+
}
91+
}
92+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const KUBERNETES_SCHEMA_URL = 'https://raw.githubusercontent.com/garethr/kubernetes-json-schema/master/v1.14.0-standalone-strict/all.json';
2+
3+
/**
4+
* Resolve kubernetes to url
5+
* @param url The URL that you want to resolve
6+
*/
7+
export function resolveURL(url: string) {
8+
if (url.toLowerCase() === 'kubernetes') {
9+
return KUBERNETES_SCHEMA_URL;
10+
}
11+
return url;
12+
}

src/languageservice/yamlLanguageService.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { YAMLCompletion } from './services/yamlCompletion';
1212
import { YAMLHover } from './services/yamlHover';
1313
import { YAMLValidation } from './services/yamlValidation';
1414
import { YAMLFormatter } from './services/yamlFormatter';
15-
import { LanguageService as JSONLanguageService, getLanguageService as getJSONLanguageService, JSONWorkerContribution } from 'vscode-json-languageservice';
15+
import { getLanguageService as getJSONLanguageService, JSONWorkerContribution } from 'vscode-json-languageservice';
16+
import { SchemaModification, SchemaAdditions, SchemaDeletions } from './apis/schemaModification';
1617

1718
export interface LanguageSettings {
1819
validate?: boolean; //Setting for whether we want to validate the schema
@@ -118,6 +119,10 @@ export interface LanguageService {
118119
doResolve(completionItem): Thenable<CompletionItem>;
119120
resetSchema(uri: string): boolean;
120121
doFormat(document: TextDocument, options: CustomFormatterOptions): TextEdit[];
122+
addSchema(schemaID: string, schema: JSONSchema): void;
123+
deleteSchema(schemaID: string): void;
124+
modifySchemaContent(schemaAdditions: SchemaAdditions): void;
125+
deleteSchemaContent(schemaDeletions: SchemaDeletions): void;
121126
}
122127

123128
export function getLanguageService(schemaRequestService: SchemaRequestService,
@@ -132,6 +137,7 @@ export function getLanguageService(schemaRequestService: SchemaRequestService,
132137
const yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService);
133138
const yamlValidation = new YAMLValidation(schemaService, promise);
134139
const formatter = new YAMLFormatter();
140+
const schemaModifier = new SchemaModification();
135141

136142
return {
137143
configure: settings => {
@@ -157,6 +163,10 @@ export function getLanguageService(schemaRequestService: SchemaRequestService,
157163
findDocumentSymbols: yamlDocumentSymbols.findDocumentSymbols.bind(yamlDocumentSymbols),
158164
findDocumentSymbols2: yamlDocumentSymbols.findHierarchicalDocumentSymbols.bind(yamlDocumentSymbols),
159165
resetSchema: (uri: string) => schemaService.onResourceChange(uri),
160-
doFormat: formatter.format.bind(formatter)
166+
doFormat: formatter.format.bind(formatter),
167+
addSchema: (schemaID: string, schema: JSONSchema) => schemaService.saveSchema(schemaID, schema),
168+
deleteSchema: (schemaID: string) => schemaService.deleteSchema(schemaID),
169+
modifySchemaContent: (schemaAdditions: SchemaAdditions) => schemaModifier.addContent(schemaService, schemaAdditions),
170+
deleteSchemaContent: (schemaDeletions: SchemaDeletions) => schemaModifier.deleteContent(schemaService, schemaDeletions)
161171
};
162172
}

src/server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { SchemaAssociationNotification, DynamicCustomSchemaRequestRegistration,
2222
import { schemaRequestHandler } from './languageservice/services/schemaRequestHandler';
2323
import { isRelativePath, relativeToAbsolutePath } from './languageservice/utils/paths';
2424
import { URI } from 'vscode-uri';
25-
25+
import { resolveURL } from './languageservice/utils/kubernetesResolver';
2626
// tslint:disable-next-line: no-any
2727
nls.config(process.env['VSCODE_NLS_CONFIG'] as any);
2828

@@ -236,8 +236,23 @@ function updateConfiguration() {
236236
* @param schema schema id
237237
* @param languageSettings current server settings
238238
*/
239-
function configureSchemas(uri: string, fileMatch: string[], schema: any, languageSettings: LanguageSettings) {
240-
uri = checkSchemaURI(uri);
239+
function configureSchemas(uri, fileMatch, schema, languageSettings){
240+
241+
uri = resolveURL(uri);
242+
243+
if (schema === null){
244+
languageSettings.schemas.push({ uri, fileMatch: fileMatch });
245+
}else {
246+
languageSettings.schemas.push({ uri, fileMatch: fileMatch, schema: schema });
247+
}
248+
249+
if (fileMatch.constructor === Array && uri === KUBERNETES_SCHEMA_URL){
250+
fileMatch.forEach(url => {
251+
specificValidatorPaths.push(url);
252+
});
253+
}else if (uri === KUBERNETES_SCHEMA_URL) {
254+
specificValidatorPaths.push(fileMatch);
255+
}
241256

242257
if (schema === null) {
243258
languageSettings.schemas.push({ uri, fileMatch: fileMatch });

test/schema.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import * as JsonSchema from '../src/languageservice/jsonSchema07';
66
import fs = require('fs');
77
import url = require('url');
88
import path = require('path');
9+
import { SchemaModification, MODIFICATION_ACTIONS, SchemaDeletions } from '../src/languageservice/apis/schemaModification';
10+
import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/kubernetesResolver';
11+
import { XHRResponse, xhr } from 'request-light';
912

1013
const fixtureDocuments = {
1114
'http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json': 'deploymentTemplate.json',
@@ -41,6 +44,13 @@ const workspaceContext = {
4144
url.resolve(resource, relativePath)
4245
};
4346

47+
const schemaRequestServiceForURL = (uri: string): Thenable<string> => {
48+
const headers = { 'Accept-Encoding': 'gzip, deflate' };
49+
return xhr({ url: uri, followRedirects: 5, headers }).then(response =>
50+
response.responseText, (error: XHRResponse) =>
51+
Promise.reject(error.responseText || error.toString()));
52+
};
53+
4454
suite('JSON Schema', () => {
4555
test('Resolving $refs', function (testDone) {
4656
const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext);
@@ -319,5 +329,158 @@ suite('JSON Schema', () => {
319329
assert.notEqual(schema, undefined);
320330
testDone();
321331
});
332+
test('Modifying schema', async () => {
333+
const service = new SchemaService.JSONSchemaService(requestServiceMock, workspaceContext);
334+
service.setSchemaContributions({
335+
schemas: {
336+
'https://myschemastore/main/schema1.json': {
337+
type: 'object',
338+
properties: {
339+
apiVersion: {
340+
type: 'string',
341+
enum: [
342+
'v1'
343+
]
344+
},
345+
kind: {
346+
type: 'string',
347+
enum: [
348+
'Pod'
349+
]
350+
}
351+
}
352+
}
353+
}
354+
});
355+
356+
const schemaModification = new SchemaModification();
357+
await schemaModification.addContent(service, {
358+
action: MODIFICATION_ACTIONS.add,
359+
path: 'properties/apiVersion',
360+
key: 'enum',
361+
content: [
362+
'v2',
363+
'v3',
364+
],
365+
schema: 'https://myschemastore/main/schema1.json'
366+
});
367+
368+
const fs = await service.getResolvedSchema('https://myschemastore/main/schema1.json');
369+
assert.deepEqual(fs.schema.properties['apiVersion'], {
370+
type: 'string',
371+
enum: ['v2', 'v3']
372+
});
373+
assert.deepEqual(fs.schema.properties['kind'], {
374+
type: 'string',
375+
enum: ['Pod']
376+
});
377+
});
378+
379+
test('Deleting schema', async () => {
380+
const service = new SchemaService.JSONSchemaService(requestServiceMock, workspaceContext);
381+
service.setSchemaContributions({
382+
schemas: {
383+
'https://myschemastore/main/schema1.json': {
384+
type: 'object',
385+
properties: {
386+
apiVersion: {
387+
type: 'string',
388+
enum: [
389+
'v1'
390+
]
391+
},
392+
kind: {
393+
type: 'string',
394+
enum: [
395+
'Pod'
396+
]
397+
}
398+
}
399+
}
400+
}
401+
});
402+
403+
const schemaModification = new SchemaModification();
404+
await schemaModification.deleteContent(service, {
405+
action: MODIFICATION_ACTIONS.delete,
406+
path: 'properties',
407+
key: 'apiVersion',
408+
schema: 'https://myschemastore/main/schema1.json'
409+
} as SchemaDeletions);
410+
411+
const fs = await service.getResolvedSchema('https://myschemastore/main/schema1.json');
412+
assert.notDeepEqual(fs.schema.properties['apiVersion'], {
413+
type: 'string',
414+
enum: ['v2', 'v3']
415+
});
416+
assert.equal(fs.schema.properties['apiVersion'], undefined);
417+
assert.deepEqual(fs.schema.properties['kind'], {
418+
type: 'string',
419+
enum: ['Pod']
420+
});
421+
});
422+
423+
test('Modifying schema works with kubernetes resolution', async () => {
424+
const service = new SchemaService.JSONSchemaService(schemaRequestServiceForURL, workspaceContext);
425+
service.registerExternalSchema(KUBERNETES_SCHEMA_URL);
426+
427+
const schemaModification = new SchemaModification();
428+
await schemaModification.addContent(service, {
429+
action: MODIFICATION_ACTIONS.add,
430+
path: 'oneOf/0/properties/kind',
431+
key: 'enum',
432+
content: [
433+
'v2',
434+
'v3',
435+
],
436+
schema: KUBERNETES_SCHEMA_URL
437+
});
438+
439+
const fs = await service.getResolvedSchema(KUBERNETES_SCHEMA_URL);
440+
assert.deepEqual(fs.schema.oneOf[0].properties['kind']['enum'], ['v2', 'v3']);
441+
});
442+
443+
test('Deleting schema works with Kubernetes resolution', async () => {
444+
const service = new SchemaService.JSONSchemaService(schemaRequestServiceForURL, workspaceContext);
445+
service.registerExternalSchema(KUBERNETES_SCHEMA_URL);
446+
447+
const schemaModification = new SchemaModification();
448+
await schemaModification.deleteContent(service, {
449+
action: MODIFICATION_ACTIONS.delete,
450+
path: 'oneOf/0/properties/kind',
451+
key: 'enum',
452+
schema: KUBERNETES_SCHEMA_URL
453+
});
454+
455+
const fs = await service.getResolvedSchema(KUBERNETES_SCHEMA_URL);
456+
assert.equal(fs.schema.oneOf[0].properties['kind']['enum'], undefined);
457+
});
458+
459+
test('Adding a brand new schema', async () => {
460+
const service = new SchemaService.JSONSchemaService(schemaRequestServiceForURL, workspaceContext);
461+
service.saveSchema('hello_world', {
462+
enum: [
463+
'test1',
464+
'test2'
465+
]
466+
});
467+
468+
const hello_world_schema = await service.getResolvedSchema('hello_world');
469+
assert.deepEqual(hello_world_schema.schema.enum, ['test1', 'test2']);
470+
});
471+
472+
test('Deleting an existing schema', async () => {
473+
const service = new SchemaService.JSONSchemaService(schemaRequestServiceForURL, workspaceContext);
474+
service.saveSchema('hello_world', {
475+
enum: [
476+
'test1',
477+
'test2'
478+
]
479+
});
480+
481+
await service.deleteSchema('hello_world');
482+
483+
const hello_world_schema = await service.getResolvedSchema('hello_world');
484+
assert.equal(hello_world_schema, null);
322485
});
323486
});

0 commit comments

Comments
 (0)