import * as os from 'os' ; import * as path from 'path' ; import { JsonFile , JsonObject } from './JsonFile' ; import { FileSystem } from './FileSystem' ; import type ValidatorType from 'z-schema' ; const Validator = require ( 'z-schema/dist/ZSchema-browser-min' ) ; interface ISchemaWithId { id : string | undefined ; } public export interface IJsonSchemaErrorInfo { /**
     * The z-schema error tree, formatted as an indented text string.
     */ details : string ; } public export interface IJsonSchemaValidateOptions { remarks customErrorHeader ? : string ; } public export interface IJsonSchemaFromFileOptions { remarks dependentSchemas ? : JsonSchema [ ] ; } remarks public export class JsonSchema { private _dependentSchemas : JsonSchema [ ] = [ ] ; private _filename : string = '' ; private _validator : ValidatorType | undefined = undefined ; private _schemaObject : JsonObject | undefined = undefined ; private constructor ( ) { } remarks public static fromFile ( filename , options ? ) { if ( ! FileSystem . exists ( filename ) ) { throw new Error ( 'Schema file not found: ' + filename ) ; } const schema = new JsonSchema ( ) ; schema . _filename = filename ; if ( options ) { schema . _dependentSchemas = options . dependentSchemas || [ ] ; } return schema ; } remarks public static fromLoadedObject ( schemaObject ) { const schema = new JsonSchema ( ) ; schema . _schemaObject = schemaObject ; return schema ; } private static _collectDependentSchemas ( collectedSchemas , dependentSchemas , seenObjects , seenIds ) { for ( const dependentSchema of dependentSchemas ) { if ( seenObjects . has ( dependentSchema ) ) { continue ; } seenObjects . add ( dependentSchema ) ; const schemaId = dependentSchema . _ensureLoaded ( ) ; if ( schemaId === '' ) { throw new Error ( `This schema ${ dependentSchema . shortName } cannot be referenced` + ' because is missing the "id" field' ) ; } if ( seenIds . has ( schemaId ) ) { throw new Error ( `This schema ${ dependentSchema . shortName } has the same "id" as another schema in this set` ) ; } seenIds . add ( schemaId ) ; collectedSchemas . push ( dependentSchema ) ; JsonSchema . _collectDependentSchemas ( collectedSchemas , dependentSchema . _dependentSchemas , seenObjects , seenIds ) ; } } /**
     * Used to nicely format the ZSchema error tree.
     */ private static _formatErrorDetails ( errorDetails ) { return JsonSchema . _formatErrorDetailsHelper ( errorDetails , '' , '' ) ; } /**
     * Used by _formatErrorDetails.
     */ private static _formatErrorDetailsHelper ( errorDetails , indent , buffer ) { for ( const errorDetail of errorDetails ) { buffer += os . EOL + indent + `Error: ${ errorDetail . path }` ; if ( errorDetail . description ) { const MAX_LENGTH = 40 ; let truncatedDescription = errorDetail . description . trim ( ) ; if ( truncatedDescription . length > MAX_LENGTH ) { truncatedDescription = truncatedDescription . substr ( 0 , MAX_LENGTH - 3 ) + '...' ; } buffer += ` (${ truncatedDescription })` ; } buffer += os . EOL + indent + `       ${ errorDetail . message }` ; if ( errorDetail . inner ) { buffer = JsonSchema . _formatErrorDetailsHelper ( errorDetail . inner , indent + '  ' , buffer ) ; } } return buffer ; } remarks public get shortName ( ) : string { if ( ! this . _filename ) { if ( this . _schemaObject ) { const schemaWithId = this . _schemaObject as ISchemaWithId ; if ( schemaWithId . id ) { return schemaWithId . id ; } } return '(anonymous schema)' ; } else { return path . basename ( this . _filename ) ; } } remarks public ensureCompiled ( ) { this . _ensureLoaded ( ) ; if ( ! this . _validator ) { const newValidator = new Validator ( { breakOnFirstError : false , noTypeless : true , noExtraKeywords : true } ) ; const anythingSchema = { type : [ 'array' , 'boolean' , 'integer' , 'number' , 'object' , 'string' ] } ; ( newValidator as any ) . setRemoteReference ( 'http://json-schema.org/draft-04/schema' , anythingSchema ) ; const collectedSchemas = [ ] ; const seenObjects = new Set < JsonSchema > ( ) ; const seenIds = new Set < string > ( ) ; JsonSchema . _collectDependentSchemas ( collectedSchemas , this . _dependentSchemas , seenObjects , seenIds ) ; for ( const collectedSchema of collectedSchemas ) { if ( ! newValidator . validateSchema ( collectedSchema . _schemaObject ) ) { throw new Error ( `Failed to validate schema "${ collectedSchema . shortName }":` + os . EOL + JsonSchema . _formatErrorDetails ( newValidator . getLastErrors ( ) ) ) ; } } this . _validator = newValidator ; } } param jsonObject param filenameForErrors param options public validateObject ( jsonObject , filenameForErrors , options ? ) { this . validateObjectWithCallback ( jsonObject , ( errorInfo ) => { const prefix = options && options . customErrorHeader ? options . customErrorHeader : 'JSON validation failed:' ; throw new Error ( prefix + os . EOL + filenameForErrors + os . EOL + errorInfo . details ) ; } ) ; } /**
     * Validates the specified JSON object against this JSON schema.  If the validation fails,
     * a callback is called for each validation error.
     */ public validateObjectWithCallback ( jsonObject , errorCallback ) { this . ensureCompiled ( ) ; if ( ! this . _validator ! . validate ( jsonObject , this . _schemaObject ) ) { const errorDetails = JsonSchema . _formatErrorDetails ( this . _validator ! . getLastErrors ( ) ) ; const args = { details : errorDetails } ; errorCallback ( args ) ; } } private _ensureLoaded ( ) { if ( ! this . _schemaObject ) { this . _schemaObject = JsonFile . load ( this . _filename ) ; } return ( this . _schemaObject as ISchemaWithId ) . id || '' ; } }