import * as Promise from 'bluebird' ; import { existsSync , readFile } from 'fs' ; import { ClientRequest , get as httpGet , IncomingMessage } from 'http' ; import { get as httpsGet } from 'https' ; import { load } from 'js-yaml' ; import * as path from 'path' ; import { ISpec , ISchema } from '../specs' ; import { IValidatorConfig , IValidatorDebugConfig } from '../validator-config' ; import { getTypeName } from './getTypeName' ; let HTTPCache = { } ; export interface ILoadCB { ( error , spec ? ) : void ; } type Primitive = boolean | number | string | null | undefined ; interface IObject { [ key ] : IObject | Primitive ; } function splitPath ( descriptor ) { const hashPos = descriptor . indexOf ( '#/' ) ; if ( hashPos !== - 1 ) { const fileName = descriptor . substr ( 0 , hashPos ) ; const path = descriptor . substr ( hashPos ) ; return [ fileName , path ] ; } return [ descriptor , undefined ] ; } export function loader ( input , config ) { if ( typeof ( input ) === 'string' ) { return < Promise < ISpec > > _loadFromString ( input , config ) ; } else if ( ! input ) { return Promise . resolve ( null ) ; } else { let spec = JSON . parse ( JSON . stringify ( input ) ) ; spec = convertRefsToAbsolute ( config . partialsDir , spec ) ; return Promise . resolve ( spec ) ; } } export function loadSchemaByName ( schemaName , spec , config ) { let schema = spec . definitions [ schemaName ] ; if ( ! schema ) { throw new Error ( `Schema ${ schemaName } not found in definitions` ) ; } return loadSchema ( schema , spec , config ) ; } export function resolveInternalPath ( path , obj ) { if ( ! path . startsWith ( '#/' ) ) { throw new Error ( `Path ${ path } not a hashed path` ) ; } path = path . substr ( 2 ) ; const components = path . split ( '/' ) ; let location = obj ; for ( let component of components ) { if ( ! ( location instanceof Object ) ) { throw new Error ( `Path ${ path } could not be found in spec, ${ component } could not be resolved` ) ; } if ( ! location . hasOwnProperty ( component ) ) { throw new Error ( `Path ${ path } could not be found in spec, ${ component } could not be resolved` ) ; } location = location [ component ] ; } return location ; } export function loadSchema ( schema , spec , config ) { if ( ! schema . $ref ) { return Promise . resolve ( schema ) ; } return _loadFromString ( schema . $ref , config , spec ) . then ( dereferencedSchema => replaceRef ( schema , dereferencedSchema ) ) ; } function replaceRef ( schema , dereferencedSchema ) { delete schema . $ref ; for ( let propertyName in dereferencedSchema ) { if ( dereferencedSchema . hasOwnProperty ( propertyName ) ) { ( < any > schema ) [ propertyName ] = ( < any > dereferencedSchema ) [ propertyName ] ; } } return schema ; } function _loadFromString ( fullPath , config , spec ? ) { let [ filePath , internalPath ] = splitPath ( fullPath ) ; const extension = path . extname ( filePath ) ; let childCwd = config . partialsDir ; let contents ; if ( ! filePath ) { if ( ! internalPath ) { return Promise . reject ( `Invalid path: ${ fullPath }` ) ; } if ( ! spec ) { return Promise . reject ( `Missing Spec to resolve ${ fullPath } against` ) ; } return loadSchema ( resolveInternalPath ( internalPath , spec ) , spec , config ) ; } else if ( _needsDownload ( filePath ) ) { contents = _download ( filePath , config ) ; } else { if ( ! path . isAbsolute ( filePath ) ) { filePath = path . join ( config . partialsDir , filePath ) ; } childCwd = path . dirname ( filePath ) ; if ( ! existsSync || ! readFile ) { return Promise . reject ( 'Cannot read file system, seems like you are using this module in frontend' ) ; } if ( ! existsSync ( filePath ) ) { return Promise . reject ( `File ${ filePath } does not exist` ) ; } contents = new Promise ( ( resolve , reject ) => { readFile ( filePath , 'utf-8' , ( err , file ) => { if ( err ) { reject ( `Error loading file ${ filePath }` ) ; } else { resolve ( file ) ; } } ) ; } ) ; } if ( extension === '.json' ) { contents = contents . then ( file => JSON . parse ( file ) ) ; } else if ( extension === '.yaml' || extension === '.yml' ) { contents = contents . then ( file => < any > load ( file ) ) ; } else { return Promise . reject ( `File extension ${ extension } is not supported` ) ; } contents = contents . then ( obj => convertRefsToAbsolute ( childCwd , obj ) ) ; if ( internalPath ) { contents = contents . then ( obj => resolveInternalPath ( internalPath , obj ) ) ; } return contents ; } function convertRefsToAbsolute ( cwd , obj ) { if ( ! cwd . endsWith ( '/' ) ) { cwd += '/' ; } switch ( getTypeName ( obj ) ) { case 'array' : for ( let prop of < Array < any > > obj ) { convertRefsToAbsolute ( cwd , prop ) ; } break ; case 'object' : if ( obj . hasOwnProperty ( '$ref' ) ) { const ref = < string > obj [ '$ref' ] ; if ( ! _needsDownload ( ref ) && ! ref . startsWith ( '/' ) && ! ref . startsWith ( '#/' ) ) { obj [ '$ref' ] = cwd + obj [ '$ref' ] ; } } for ( let propertyName in obj ) { convertRefsToAbsolute ( cwd , obj [ propertyName ] ) ; } break ; default : break ; } return obj ; } function _needsDownload ( path ) { return path . indexOf ( 'http://' ) === 0 || path . indexOf ( 'https://' ) === 0 ; } function _download ( url , config ) { if ( ! config . disableDownloadCache && HTTPCache [ url ] ) { return HTTPCache [ url ] ; } let downloadMethod ; if ( url . indexOf ( 'http://' ) === 0 ) { if ( config . disallowHttp ) { return Promise . reject ( 'Definition needs HTTP, which is disallowed' ) ; } downloadMethod = httpGet ; } else { if ( config . disallowHttps ) { return Promise . reject ( 'Definition needs HTTPS, which is disallowed' ) ; } downloadMethod = httpsGet ; } let loadPromise = new Promise < string > ( ( resolve , reject ) => { downloadMethod ( url , ( response ) => _downloadStarted ( response , config , resolve , reject ) ) . on ( 'error' , function ( err ) { if ( ! config . disableDownloadCache ) { delete HTTPCache [ url ] ; } return reject ( err . message ) ; } ) ; } ) ; if ( ! config . disableDownloadCache ) { HTTPCache [ url ] = loadPromise ; } return loadPromise ; } function _downloadStarted ( response , config , resolve , reject ) { response . setEncoding ( 'utf8' ) ; let file = '' ; response . on ( 'data' , ( chunk ) => { file += chunk ; } ) ; response . on ( 'end' , ( ) => { resolve ( file ) ; } ) ; }