'use strict' ; import { JSONSchema , JSONSchemaMap , JSONSchemaRef } from '../jsonSchema' ; import { SchemaPriority , SchemaRequestService , WorkspaceContextService } from '../yamlLanguageService' ; import { UnresolvedSchema , ResolvedSchema , JSONSchemaService , SchemaDependencies , ISchemaContributions , SchemaHandle , } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService' ; import { URI } from 'vscode-uri' ; import * as nls from 'vscode-nls' ; import { convertSimple2RegExpPattern } from '../utils/strings' ; import { SingleYAMLDocument } from '../parser/yamlParser07' ; import { JSONDocument } from '../parser/jsonParser07' ; import { parse } from 'yaml' ; import * as path from 'path' ; import { getSchemaFromModeline } from './modelineUtil' ; import { JSONSchemaDescriptionExt } from '../../requestTypes' ; const localize = nls . loadMessageBundle ( ) ; export declare type CustomSchemaProvider = ( uri : string ) => Promise < string | string [ ] > ; export enum MODIFICATION_ACTIONS { 'delete' , 'add' , 'deleteAll' } export interface SchemaAdditions { schema : string ; action : MODIFICATION_ACTIONS . add ; path : string ; key : string ; content : any ; } export interface SchemaDeletions { schema : string ; action : MODIFICATION_ACTIONS . delete ; path : string ; key : string ; } export interface SchemaDeletionsAll { schemas : string [ ] ; action : MODIFICATION_ACTIONS . deleteAll ; } export class FilePatternAssociation { private schemas : string [ ] ; private patternRegExp : RegExp ; constructor ( pattern ) { try { this . patternRegExp = new RegExp ( convertSimple2RegExpPattern ( pattern ) + '$' ) ; } catch ( e ) { this . patternRegExp = null ; } this . schemas = [ ] ; } public addSchema ( id ) { this . schemas . push ( id ) ; } public matchesPattern ( fileName ) { return this . patternRegExp && this . patternRegExp . test ( fileName ) ; } public getSchemas ( ) { return this . schemas ; } } export class YAMLSchemaService extends JSONSchemaService { [ x ] : any ; private customSchemaProvider : CustomSchemaProvider | undefined ; private filePatternAssociations : JSONSchemaService . FilePatternAssociation [ ] ; private contextService : WorkspaceContextService ; private requestService : SchemaRequestService ; public schemaPriorityMapping : Map < string , Set < SchemaPriority > > ; private schemaUriToNameAndDescription = new Map < string , [ string , string ] > ( ) ; constructor ( requestService , contextService ? , promiseConstructor ? ) { super ( requestService , contextService , promiseConstructor ) ; this . customSchemaProvider = undefined ; this . requestService = requestService ; this . schemaPriorityMapping = new Map ( ) ; } registerCustomSchemaProvider ( customSchemaProvider ) { this . customSchemaProvider = customSchemaProvider ; } getAllSchemas ( ) { const result = [ ] ; const schemaUris = new Set < string > ( ) ; for ( const filePattern of this . filePatternAssociations ) { const schemaUri = filePattern . uris [ 0 ] ; if ( schemaUris . has ( schemaUri ) ) { continue ; } schemaUris . add ( schemaUri ) ; const schemaHandle = { uri : schemaUri , fromStore : false , usedForCurrentFile : false , } ; if ( this . schemaUriToNameAndDescription . has ( schemaUri ) ) { const [ name , description ] = this . schemaUriToNameAndDescription . get ( schemaUri ) ; schemaHandle . name = name ; schemaHandle . description = description ; schemaHandle . fromStore = true ; } result . push ( schemaHandle ) ; } return result ; } async resolveSchemaContent ( schemaToResolve , schemaURL , dependencies ) { const resolveErrors = schemaToResolve . errors . slice ( 0 ) ; let schema = schemaToResolve . schema ; const contextService = this . contextService ; const findSection = ( schema , path ) : JSONSchema => { if ( ! path ) { return schema ; } let current = schema ; if ( path [ 0 ] === '/' ) { path = path . substr ( 1 ) ; } path . split ( '/' ) . some ( ( part ) => { current = current [ part ] ; return ! current ; } ) ; return current ; } ; const merge = ( target , sourceRoot , sourceURI , path ) : void => { const section = findSection ( sourceRoot , path ) ; if ( section ) { for ( const key in section ) { if ( Object . prototype . hasOwnProperty . call ( section , key ) && ! Object . prototype . hasOwnProperty . call ( target , key ) ) { target [ key ] = section [ key ] ; } } } else { resolveErrors . push ( localize ( 'json.schema.invalidref' , "$ref '{0}' in '{1}' can not be resolved." , path , sourceURI ) ) ; } } ; const resolveExternalLink = ( node , uri , linkPath , parentSchemaURL , parentSchemaDependencies ) : Promise < any > => { if ( contextService && ! /^\w+:\/\/.*/ . test ( uri ) ) { uri = contextService . resolveRelativePath ( uri , parentSchemaURL ) ; } uri = this . normalizeId ( uri ) ; const referencedHandle = this . getOrAddSchemaHandle ( uri ) ; return referencedHandle . getUnresolvedSchema ( ) . then ( ( unresolvedSchema ) => { parentSchemaDependencies [ uri ] = true ; if ( unresolvedSchema . errors . length ) { const loc = linkPath ? uri + '#' + linkPath : uri ; resolveErrors . push ( localize ( 'json.schema.problemloadingref' , "Problems loading reference '{0}': {1}" , loc , unresolvedSchema . errors [ 0 ] ) ) ; } merge ( node , unresolvedSchema . schema , uri , linkPath ) ; node . url = uri ; return resolveRefs ( node , unresolvedSchema . schema , uri , referencedHandle . dependencies ) ; } ) ; } ; const resolveRefs = async ( node , parentSchema , parentSchemaURL , parentSchemaDependencies ) : Promise < any > => { if ( ! node || typeof node !== 'object' ) { return null ; } const toWalk = [ node ] ; const seen = [ ] ; const openPromises = [ ] ; const collectEntries = ( ... entries ) : void => { for ( const entry of entries ) { if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } ; const collectMapEntries = ( ... maps ) : void => { for ( const map of maps ) { if ( typeof map === 'object' ) { for ( const key in map ) { const entry = map [ key ] ; if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } } } ; const collectArrayEntries = ( ... arrays ) : void => { for ( const array of arrays ) { if ( Array . isArray ( array ) ) { for ( const entry of array ) { if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } } } ; const handleRef = ( next ) : void => { const seenRefs = [ ] ; while ( next . $ref ) { const ref = next . $ref ; const segments = ref . split ( '#' , 2 ) ; next . _$ref = next . $ref ; delete next . $ref ; if ( segments [ 0 ] . length > 0 ) { openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ) ; return ; } else { if ( seenRefs . indexOf ( ref ) === - 1 ) { merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ; seenRefs . push ( ref ) ; } } } collectEntries ( < JSONSchema > next . items , next . additionalItems , < JSONSchema > next . additionalProperties , next . not , next . contains , next . propertyNames , next . if , next . then , next . else ) ; collectMapEntries ( next . definitions , next . properties , next . patternProperties , < JSONSchemaMap > next . dependencies ) ; collectArrayEntries ( next . anyOf , next . allOf , next . oneOf , < JSONSchema [ ] > next . items , next . schemaSequence ) ; } ; if ( parentSchemaURL . indexOf ( '#' ) > 0 ) { const segments = parentSchemaURL . split ( '#' , 2 ) ; if ( segments [ 0 ] . length > 0 && segments [ 1 ] . length > 0 ) { const newSchema = { } ; await resolveExternalLink ( newSchema , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ; for ( const key in schema ) { if ( key === 'required' ) { continue ; } if ( Object . prototype . hasOwnProperty . call ( schema , key ) && ! Object . prototype . hasOwnProperty . call ( newSchema , key ) ) { newSchema [ key ] = schema [ key ] ; } } schema = newSchema ; } } while ( toWalk . length ) { const next = toWalk . pop ( ) ; if ( seen . indexOf ( next ) >= 0 ) { continue ; } seen . push ( next ) ; handleRef ( next ) ; } return Promise . all ( openPromises ) ; } ; await resolveRefs ( schema , schema , schemaURL , dependencies ) ; return new ResolvedSchema ( schema , resolveErrors ) ; } public getSchemaForResource ( resource , doc ) { const resolveSchema = ( ) : any => { const seen = Object . create ( null ) ; const schemas = [ ] ; let schemaFromModeline = getSchemaFromModeline ( doc ) ; if ( schemaFromModeline !== undefined ) { if ( ! schemaFromModeline . startsWith ( 'file:' ) && ! schemaFromModeline . startsWith ( 'http' ) ) { let appendix = '' ; if ( schemaFromModeline . indexOf ( '#' ) > 0 ) { const segments = schemaFromModeline . split ( '#' , 2 ) ; schemaFromModeline = segments [ 0 ] ; appendix = segments [ 1 ] ; } if ( ! path . isAbsolute ( schemaFromModeline ) ) { const resUri = URI . parse ( resource ) ; schemaFromModeline = URI . file ( path . resolve ( path . parse ( resUri . fsPath ) . dir , schemaFromModeline ) ) . toString ( ) ; } else { schemaFromModeline = URI . file ( schemaFromModeline ) . toString ( ) ; } if ( appendix . length > 0 ) { schemaFromModeline += '#' + appendix ; } } this . addSchemaPriority ( schemaFromModeline , SchemaPriority . Modeline ) ; schemas . push ( schemaFromModeline ) ; seen [ schemaFromModeline ] = true ; } for ( const entry of this . filePatternAssociations ) { if ( entry . matchesPattern ( resource ) ) { for ( const schemaId of entry . getURIs ( ) ) { if ( ! seen [ schemaId ] ) { schemas . push ( schemaId ) ; seen [ schemaId ] = true ; } } } } /**
             * If this resource matches a schemaID directly then use that schema.
             * This will be used in the case where the yaml language server is being used as a library
             * and clients want to save a schema with a particular ID and also use that schema
             * in language features
             */ const normalizedResourceID = this . normalizeId ( resource ) ; if ( this . schemasById [ normalizedResourceID ] ) { schemas . push ( normalizedResourceID ) ; } if ( schemas . length > 0 ) { const highestPrioSchemas = this . highestPrioritySchemas ( schemas ) ; const schemaHandle = super . createCombinedSchema ( resource , highestPrioSchemas ) ; return schemaHandle . getResolvedSchema ( ) . then ( ( schema ) => { if ( schema . schema && typeof schema . schema !== 'string' ) { schema . schema . url = schemaHandle . url ; } if ( schema . schema && schema . schema . schemaSequence && schema . schema . schemaSequence [ ( < SingleYAMLDocument > doc ) . currentDocIndex ] ) { return new ResolvedSchema ( schema . schema . schemaSequence [ ( < SingleYAMLDocument > doc ) . currentDocIndex ] ) ; } return schema ; } ) ; } return Promise . resolve ( null ) ; } ; if ( this . customSchemaProvider ) { return this . customSchemaProvider ( resource ) . then ( ( schemaUri ) => { if ( Array . isArray ( schemaUri ) ) { if ( schemaUri . length === 0 ) { return resolveSchema ( ) ; } return Promise . all ( schemaUri . map ( ( schemaUri ) => { return this . resolveCustomSchema ( schemaUri , doc ) ; } ) ) . then ( ( schemas ) => { return { errors : [ ] , schema : { anyOf : schemas . map ( ( schemaObj ) => { return schemaObj . schema ; } ) , } , } ; } , ( ) => { return resolveSchema ( ) ; } ) ; } if ( ! schemaUri ) { return resolveSchema ( ) ; } return this . resolveCustomSchema ( schemaUri , doc ) ; } ) . then ( ( schema ) => { return schema ; } , ( ) => { return resolveSchema ( ) ; } ) ; } else { return resolveSchema ( ) ; } } public addSchemaPriority ( uri , priority ) { let currSchemaArray = this . schemaPriorityMapping . get ( uri ) ; if ( currSchemaArray ) { currSchemaArray = currSchemaArray . add ( priority ) ; this . schemaPriorityMapping . set ( uri , currSchemaArray ) ; } else { this . schemaPriorityMapping . set ( uri , new Set < SchemaPriority > ( ) . add ( priority ) ) ; } } /**
     * Search through all the schemas and find the ones with the highest priority
     */ private highestPrioritySchemas ( schemas ) { let highestPrio = 0 ; const priorityMapping = new Map < SchemaPriority , string [ ] > ( ) ; schemas . forEach ( ( schema ) => { const priority = this . schemaPriorityMapping . get ( schema ) || [ 0 ] ; priority . forEach ( ( prio ) => { if ( prio > highestPrio ) { highestPrio = prio ; } let currPriorityArray = priorityMapping . get ( prio ) ; if ( currPriorityArray ) { currPriorityArray = ( currPriorityArray as string [ ] ) . concat ( schema ) ; priorityMapping . set ( prio , currPriorityArray ) ; } else { priorityMapping . set ( prio , [ schema ] ) ; } } ) ; } ) ; return priorityMapping . get ( highestPrio ) || [ ] ; } private async resolveCustomSchema ( schemaUri , doc ) { const unresolvedSchema = await this . loadSchema ( schemaUri ) ; const schema = await this . resolveSchemaContent ( unresolvedSchema , schemaUri , [ ] ) ; if ( schema . schema ) { schema . schema . url = schemaUri ; } if ( schema . schema && schema . schema . schemaSequence && schema . schema . schemaSequence [ doc . currentDocIndex ] ) { return new ResolvedSchema ( schema . schema . schemaSequence [ doc . currentDocIndex ] ) ; } return schema ; } /**
     * Save a schema with schema ID and schema content.
     * Overrides previous schemas set for that schema ID.
     */ public async saveSchema ( schemaId , schemaContent ) { const id = this . normalizeId ( schemaId ) ; this . getOrAddSchemaHandle ( id , schemaContent ) ; this . schemaPriorityMapping . set ( id , new Set < SchemaPriority > ( ) . add ( SchemaPriority . Settings ) ) ; return Promise . resolve ( undefined ) ; } /**
     * Delete schemas on specific path
     */ public async deleteSchemas ( deletions ) { deletions . schemas . forEach ( ( s ) => { this . deleteSchema ( s ) ; } ) ; return Promise . resolve ( undefined ) ; } /**
     * Delete a schema with schema ID.
     */ public async deleteSchema ( schemaId ) { const id = this . normalizeId ( schemaId ) ; if ( this . schemasById [ id ] ) { delete this . schemasById [ id ] ; } this . schemaPriorityMapping . delete ( id ) ; return Promise . resolve ( undefined ) ; } /**
     * Add content to a specified schema at a specified path
     */ public async addContent ( additions ) { const schema = await this . getResolvedSchema ( additions . schema ) ; if ( schema ) { const resolvedSchemaLocation = this . resolveJSONSchemaToSection ( schema . schema , additions . path ) ; if ( typeof resolvedSchemaLocation === 'object' ) { resolvedSchemaLocation [ additions . key ] = additions . content ; } await this . saveSchema ( additions . schema , schema . schema ) ; } } /**
     * Delete content in a specified schema at a specified path
     */ public async deleteContent ( deletions ) { const schema = await this . getResolvedSchema ( deletions . schema ) ; if ( schema ) { const resolvedSchemaLocation = this . resolveJSONSchemaToSection ( schema . schema , deletions . path ) ; if ( typeof resolvedSchemaLocation === 'object' ) { delete resolvedSchemaLocation [ deletions . key ] ; } await this . saveSchema ( deletions . schema , schema . schema ) ; } } returns private resolveJSONSchemaToSection ( schema , paths ) { const splitPathway = paths . split ( '/' ) ; let resolvedSchemaLocation = schema ; for ( const path of splitPathway ) { if ( path === '' ) { continue ; } this . resolveNext ( resolvedSchemaLocation , path ) ; resolvedSchemaLocation = resolvedSchemaLocation [ path ] ; } return resolvedSchemaLocation ; } param object param token private resolveNext ( object , token ) { if ( Array . isArray ( object ) && isNaN ( token ) ) { throw new Error ( 'Expected a number after the array object' ) ; } else if ( typeof object === 'object' && typeof token !== 'string' ) { throw new Error ( 'Expected a string after the object' ) ; } } /**
     * Everything below here is needed because we're importing from vscode-json-languageservice umd and we need
     * to provide a wrapper around the javascript methods we are calling since they have no type
     */ normalizeId ( id ) { try { return URI . parse ( id ) . toString ( ) ; } catch ( e ) { return id ; } } getOrAddSchemaHandle ( id , unresolvedSchemaContent ? ) { return super . getOrAddSchemaHandle ( id , unresolvedSchemaContent ) ; } loadSchema ( schemaUri ) { const requestService = this . requestService ; return super . loadSchema ( schemaUri ) . then ( ( unresolvedJsonSchema ) => { if ( unresolvedJsonSchema . errors && unresolvedJsonSchema . schema === undefined ) { return requestService ( schemaUri ) . then ( ( content ) => { if ( ! content ) { const errorMessage = localize ( 'json.schema.nocontent' , "Unable to load schema from '{0}': No content." , toDisplayString ( schemaUri ) ) ; return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ; } try { const schemaContent = parse ( content ) ; return new UnresolvedSchema ( schemaContent , [ ] ) ; } catch ( yamlError ) { const errorMessage = localize ( 'json.schema.invalidFormat' , "Unable to parse content from '{0}': {1}." , toDisplayString ( schemaUri ) , yamlError ) ; return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ; } } , ( error ) => { let errorMessage = error . toString ( ) ; const errorSplit = error . toString ( ) . split ( 'Error: ' ) ; if ( errorSplit . length > 1 ) { errorMessage = errorSplit [ 1 ] ; } return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ; } ) ; } unresolvedJsonSchema . uri = schemaUri ; if ( this . schemaUriToNameAndDescription . has ( schemaUri ) ) { const [ name , description ] = this . schemaUriToNameAndDescription . get ( schemaUri ) ; unresolvedJsonSchema . schema . title = name ?? unresolvedJsonSchema . schema . title ; unresolvedJsonSchema . schema . description = description ?? unresolvedJsonSchema . schema . description ; } return unresolvedJsonSchema ; } ) ; } registerExternalSchema ( uri , filePatterns ? , unresolvedSchema ? , name ? , description ? ) { if ( name || description ) { this . schemaUriToNameAndDescription . set ( uri , [ name , description ] ) ; } return super . registerExternalSchema ( uri , filePatterns , unresolvedSchema ) ; } clearExternalSchemas ( ) { super . clearExternalSchemas ( ) ; } setSchemaContributions ( schemaContributions ) { super . setSchemaContributions ( schemaContributions ) ; } getRegisteredSchemaIds ( filter ? ) { return super . getRegisteredSchemaIds ( filter ) ; } getResolvedSchema ( schemaId ) { return super . getResolvedSchema ( schemaId ) ; } onResourceChange ( uri ) { return super . onResourceChange ( uri ) ; } } function toDisplayString ( url ) { try { const uri = URI . parse ( url ) ; if ( uri . scheme === 'file' ) { return uri . fsPath ; } } catch ( e ) { } return url ; }