import * as Json from "jsonc-parser" ; import forEach = require ( "lodash/forEach" ) ; import noop = require ( "lodash/noop" ) ; import { DocumentType , JSONSchema , JSONSchemaMap , YAMLDocument } from "@serverless-ide/config" ; import { TextDocument } from "vscode-languageserver" ; import * as nls from "vscode-nls" ; import URI from "vscode-uri" ; import { sendException } from "../analytics" ; import requestService from "../request" ; import { applyDocumentMutations } from "./mutation" ; const localize = nls . loadMessageBundle ( ) ; function getParseErrorMessage ( errorCode ) { switch ( errorCode ) { case Json . ParseErrorCode . InvalidSymbol : return localize ( "error.invalidSymbol" , "Invalid symbol" ) ; case Json . ParseErrorCode . InvalidNumberFormat : return localize ( "error.invalidNumberFormat" , "Invalid number format" ) ; case Json . ParseErrorCode . PropertyNameExpected : return localize ( "error.propertyNameExpected" , "Property name expected" ) ; case Json . ParseErrorCode . ValueExpected : return localize ( "error.valueExpected" , "Value expected" ) ; case Json . ParseErrorCode . ColonExpected : return localize ( "error.colonExpected" , "Colon expected" ) ; case Json . ParseErrorCode . CommaExpected : return localize ( "error.commaExpected" , "Comma expected" ) ; case Json . ParseErrorCode . CloseBraceExpected : return localize ( "error.closeBraceExpected" , "Closing brace expected" ) ; case Json . ParseErrorCode . CloseBracketExpected : return localize ( "error.closeBracketExpected" , "Closing bracket expected" ) ; case Json . ParseErrorCode . EndOfFileExpected : return localize ( "error.endOfFileExpected" , "End of file expected" ) ; default : return "" ; } } export class UnresolvedSchema { schema : JSONSchema ; errors : string [ ] ; constructor ( schema , errors = [ ] ) { this . schema = schema ; this . errors = errors ; } } export class ResolvedSchema { schema : JSONSchema ; errors : string [ ] ; constructor ( schema , errors = [ ] ) { this . schema = schema ; this . errors = errors ; } getLastDescription ( path ) { let description = undefined ; this . getSectionRecursive ( path , this . schema , schemaNode => { if ( schemaNode && schemaNode . description ) { description = schemaNode . description ; } } ) ; return description ; } getSection ( path ) { return this . getSectionRecursive ( path , this . schema ) ; } private getSectionRecursive ( path , schema , visitor = noop ) { visitor ( schema ) ; if ( ! schema || path . length === 0 ) { return schema ; } if ( schema . oneOf && Array . isArray ( schema . oneOf ) ) { for ( const oneOfEntry of schema . oneOf ) { const result = this . getSectionRecursive ( path . slice ( ) , oneOfEntry , visitor ) ; if ( result ) { return result ; } } return null ; } const next = path . shift ( ) ; if ( ! next ) { return null ; } if ( schema . properties && schema . properties [ next ] ) { return this . getSectionRecursive ( path , schema . properties [ next ] , visitor ) ; } else if ( schema . patternProperties ) { Object . keys ( schema . patternProperties ) . forEach ( pattern => { const regex = new RegExp ( pattern ) ; if ( regex . test ( next ) ) { return this . getSectionRecursive ( path , schema . patternProperties [ pattern ] , visitor ) ; } } ) ; } else if ( schema . additionalProperties ) { return this . getSectionRecursive ( path , schema . additionalProperties , visitor ) ; } else if ( next . match ( "[0-9]+" ) ) { if ( schema . items ) { return this . getSectionRecursive ( path , schema . items , visitor ) ; } else if ( Array . isArray ( schema . items ) ) { try { const index = parseInt ( next , 10 ) ; if ( schema . items [ index ] ) { return this . getSectionRecursive ( path , schema . items [ index ] , visitor ) ; } return null ; } catch ( err ) { sendException ( err ) ; return null ; } } } return null ; } } const resolveSchemaContent = async ( schemaToResolve ) : Promise < ResolvedSchema > => { const resolveErrors = schemaToResolve . errors . slice ( 0 ) ; const schema = schemaToResolve . schema ; const findSection = ( selectonSchema , path ) : JSONSchema | false => { if ( ! path ) { return selectonSchema ; } let current = selectonSchema ; if ( path [ 0 ] === "/" ) { path = path . substr ( 1 ) ; } path . split ( "/" ) . some ( part => { current = current [ part ] ; return ! current ; } ) ; return current ; } ; const resolveLink = ( node , linkedSchema , linkPath ) : void => { const section = findSection ( linkedSchema , linkPath ) ; if ( section ) { for ( const key in section ) { if ( section . hasOwnProperty ( key ) && ! node . hasOwnProperty ( key ) ) { node [ key ] = section [ key ] ; } } } else { resolveErrors . push ( localize ( "json.schema.invalidref" , "$ref '{0}' in {1} can not be resolved." , linkPath , linkedSchema . id ) ) ; } delete node . $ref ; } ; const resolveRefs = ( node , parentSchema ) : Promise < any > => { if ( ! node ) { return Promise . resolve ( null ) ; } const toWalk = [ node ] ; const seen = [ ] ; const openPromises = [ ] ; const collectEntries = ( ... entries ) => { for ( const entry of entries ) { if ( typeof entry === "object" ) { toWalk . push ( entry ) ; } } } ; const collectMapEntries = ( ... maps ) => { for ( const map of maps ) { if ( typeof map === "object" ) { forEach ( map , ( value , key ) => { const entry = map [ key ] ; toWalk . push ( entry ) ; } ) ; } } } ; const collectArrayEntries = ( ... arrays ) => { for ( const array of arrays ) { if ( Array . isArray ( array ) ) { toWalk . push . apply ( toWalk , array ) ; } } } ; while ( toWalk . length ) { const next = toWalk . pop ( ) ; if ( seen . indexOf ( next ) >= 0 ) { continue ; } seen . push ( next ) ; if ( next . $ref ) { const segments = next . $ref . split ( "#" , 2 ) ; resolveLink ( next , parentSchema , segments [ 1 ] ) ; } collectEntries ( next . items , next . additionalProperties , next . not ) ; collectMapEntries ( next . definitions , next . properties , next . patternProperties , next . dependencies as JSONSchemaMap ) ; collectArrayEntries ( next . anyOf , next . allOf , next . oneOf , next . items as JSONSchema [ ] ) ; } return Promise . all ( openPromises ) ; } ; await resolveRefs ( schema , schema ) ; return new ResolvedSchema ( schema , resolveErrors ) ; } ; interface PartialSchema { properties : { [ key ] : ResolvedSchema ; } ; global ? : ResolvedSchema ; } export class JSONSchemaService { private schemas : { [ Key in DocumentType ] : Promise < ResolvedSchema | void > ; } ; private partialSchemas : { [ uri ] : PartialSchema ; } ; constructor ( ) { const samSchema = require ( "@serverless-ide/sam-schema/schema.json" ) as JSONSchema ; const cloudformationSchema = require ( "@serverless-ide/cloudformation-schema/schema.json" ) as JSONSchema ; const serverlessFrameworkSchema = require ( "@serverless-ide/serverless-framework-schema/schema.json" ) as JSONSchema ; this . schemas = { [ DocumentType . CLOUD_FORMATION ] : resolveSchemaContent ( new UnresolvedSchema ( cloudformationSchema ) ) , [ DocumentType . SAM ] : resolveSchemaContent ( new UnresolvedSchema ( samSchema ) ) , [ DocumentType . SERVERLESS_FRAMEWORK ] : resolveSchemaContent ( new UnresolvedSchema ( serverlessFrameworkSchema ) ) , [ DocumentType . UNKNOWN ] : Promise . resolve ( undefined ) } ; this . partialSchemas = { } ; } async getSchemaForDocument ( document , yamlDocument ) { if ( this . partialSchemas [ document . uri ] ) { return this . getPartialSchemaForDocumentUri ( document . uri ) ; } else { const schema = await this . getSchemaForDocumentType ( yamlDocument . documentType ) ; if ( schema ) { return applyDocumentMutations ( schema , yamlDocument ) ; } } } async getPartialSchemaForDocumentUri ( uri ) { const partialSchema = this . partialSchemas [ uri ] ; if ( partialSchema ) { let schema = { allOf : [ ] } ; if ( partialSchema . global ) { schema . allOf . push ( partialSchema . global . schema ) ; } forEach ( partialSchema . properties , ( propertySchema , key ) => { schema . allOf . push ( { type : "object" , properties : { [ key ] : propertySchema . schema } , required : [ key ] } ) ; } ) ; if ( schema . allOf . length === 1 ) { schema = schema . allOf [ 0 ] ; } return new ResolvedSchema ( schema ) ; } } registerPartialSchema ( uri , schema , property ? ) { const resolvedSchema = new ResolvedSchema ( schema ) ; if ( ! this . partialSchemas [ uri ] ) { this . partialSchemas [ uri ] = { properties : { } } ; } if ( property ) { this . partialSchemas [ uri ] . properties [ property ] = resolvedSchema ; } else { this . partialSchemas [ uri ] . global = resolvedSchema ; } } clearPartialSchema ( uri ) { delete this . partialSchemas [ uri ] ; } private async getSchemaForDocumentType ( documentType ) { return await this . schemas [ documentType ] ; } private async loadSchema ( url ) { try { const content = await requestService ( url ) ; if ( ! content ) { const errorMessage = localize ( "json.schema.nocontent" , "Unable to load schema from '{0}': No content." , toDisplayString ( url ) ) ; const defaultSchema = { } ; return new UnresolvedSchema ( defaultSchema , [ errorMessage ] ) ; } let schemaContent = { } ; const jsonErrors = [ ] ; schemaContent = Json . parse ( content , jsonErrors ) ; const errors = jsonErrors . length ? [ localize ( "json.schema.invalidFormat" , "Unable to parse content from '{0}': {1}." , toDisplayString ( url ) , getParseErrorMessage ( jsonErrors [ 0 ] ) ) ] : [ ] ; return new UnresolvedSchema ( schemaContent , errors ) ; } catch ( error ) { const errorMessage = localize ( "json.schema.unabletoload" , "Unable to load schema from '{0}': {1}" , toDisplayString ( url ) , error . toString ( ) ) ; const defaultSchema = { } ; return new UnresolvedSchema ( defaultSchema , [ errorMessage ] ) ; } } } function toDisplayString ( url ) { try { const uri = URI . parse ( url ) ; if ( uri . scheme === "file" ) { return uri . fsPath ; } } catch ( e ) { } return url ; }