import { ErrorTypes , ErrorMessages , TypeAssertionErrorMessageConstraints , TypeAssertion , RepeatedAssertion , SpreadAssertion , OptionalAssertion , ValidationContext } from '../types' ; import { escapeString } from './escape' ; import { nvl } from './util' ; export const errorTypeNames = [ '' , 'InvalidDefinition' , 'Required' , 'TypeUnmatched' , 'AdditionalPropUnmatched' , 'RepeatQtyUnmatched' , 'SequenceUnmatched' , 'ValueRangeUnmatched' , 'ValuePatternUnmatched' , 'ValueLengthUnmatched' , 'ValueUnmatched' , ] ; export const defaultMessages = { invalidDefinition : '"%{name}" of "%{parentType}" type definition is invalid.' , required : '"%{name}" of "%{parentType}" is required.' , typeUnmatched : '"%{name}" of "%{parentType}" should be type "%{expectedType}".' , additionalPropUnmatched : '"%{addtionalProps}" of "%{parentType}" are not matched to additional property patterns.' , repeatQtyUnmatched : '"%{name}" of "%{parentType}" should repeat %{repeatQty} times.' , sequenceUnmatched : '"%{name}" of "%{parentType}" sequence is not matched' , valueRangeUnmatched : '"%{name}" of "%{parentType}" value should be in the range %{minValue} to %{maxValue}.' , valuePatternUnmatched : '"%{name}" of "%{parentType}" value should be matched to pattern "%{pattern}"' , valueLengthUnmatched : '"%{name}" of "%{parentType}" length should be in the range %{minLength} to %{maxLength}.' , valueUnmatched : '"%{name}" of "%{parentType}" value should be "%{expectedValue}".' , } ; type TopRepeatable = RepeatedAssertion | SpreadAssertion | OptionalAssertion | null ; interface ReportErrorArguments { ctx : ValidationContext ; substitutions ? : [ [ string , string ] ] ; } function getErrorMessage ( errType , ... messages ) { for ( const m of messages ) { switch ( errType ) { case ErrorTypes . InvalidDefinition : if ( m . invalidDefinition ) { return m . invalidDefinition ; } break ; case ErrorTypes . Required : if ( m . required ) { return m . required ; } break ; case ErrorTypes . TypeUnmatched : if ( m . typeUnmatched ) { return m . typeUnmatched ; } break ; case ErrorTypes . AdditionalPropUnmatched : if ( m . additionalPropUnmatched ) { return m . additionalPropUnmatched ; } break ; case ErrorTypes . RepeatQtyUnmatched : if ( m . repeatQtyUnmatched ) { return m . repeatQtyUnmatched ; } break ; case ErrorTypes . SequenceUnmatched : if ( m . sequenceUnmatched ) { return m . sequenceUnmatched ; } break ; case ErrorTypes . ValueRangeUnmatched : if ( m . valueRangeUnmatched ) { return m . valueRangeUnmatched ; } break ; case ErrorTypes . ValuePatternUnmatched : if ( m . valuePatternUnmatched ) { return m . valuePatternUnmatched ; } break ; case ErrorTypes . ValueLengthUnmatched : if ( m . valueLengthUnmatched ) { return m . valueLengthUnmatched ; } break ; case ErrorTypes . ValueUnmatched : if ( m . valueUnmatched ) { return m . valueUnmatched ; } break ; } } return '' ; } function findTopRepeatableAssertion ( ctx ) { const ret = ctx . typeStack . slice ( ) . reverse ( ) . map ( x => Array . isArray ( x ) ? x [ 0 ] : x ) . find ( x => x . kind === 'repeated' || x . kind === 'spread' || x . kind === 'optional' ) as RepeatedAssertion | SpreadAssertion | OptionalAssertion || null ; return ret ; } function getExpectedType ( ty ) { switch ( ty . kind ) { case 'repeated' : return `(repeated ${ getExpectedType ( ty . repeated ) })` ; case 'spread' : return getExpectedType ( ty . spread ) ; case 'sequence' : return '(sequence)' ; case 'primitive' : return ty . primitiveName ; case 'primitive-value' : return `(value ${ typeof ty . value === 'string' ? `'${ String ( ty . value ) }'` : String ( ty . value ) })` ; case 'optional' : return getExpectedType ( ty . optional ) ; case 'one-of' : return `(one of ${ ty . oneOf . map ( x => getExpectedType ( x ) ) . join ( ', ' ) })` ; case 'never' : case 'any' : case 'unknown' : return ty . kind ; case 'symlink' : return ty . symlinkTargetName ; default : return ty . typeName ? ty . typeName : '?' ; } } export function formatErrorMessage ( msg , data , ty , args , values ) { let ret = msg ; const tr = values . topRepeatable ; const dict = new Map < string , string > ( [ [ 'expectedType' , ty . stereotype ? ty . stereotype : escapeString ( getExpectedType ( ty ) ) ] , [ 'type' , escapeString ( typeof data ) ] , [ 'expectedValue' , escapeString ( ty . kind === 'primitive-value' ? String ( ty . value ) : ty . kind === 'enum' ? ty . typeName ? `enum member of ${ ty . typeName }` : '?' : '?' ) ] , [ 'value' , escapeString ( String ( data ) ) ] , [ 'repeatQty' , escapeString ( tr ? tr . kind !== 'optional' ? `${ nvl ( tr . min , '' ) }${ ( tr . min !== null && tr . min !== void 0 ) || ( tr . max !== null && tr . max !== void 0 ) ? '..' : '' }${ nvl ( tr . max , '' ) }` : '0..1' : '?' ) ] , [ 'minValue' , escapeString ( ty . kind === 'primitive' ? `${ nvl ( ty . minValue , nvl ( ty . greaterThanValue , '(smallest)' ) ) }` : '?' ) ] , [ 'maxValue' , escapeString ( ty . kind === 'primitive' ? `${ nvl ( ty . maxValue , nvl ( ty . lessThanValue , '(biggest)' ) ) }` : '?' ) ] , [ 'pattern' , escapeString ( ty . kind === 'primitive' ? `${ ty . pattern ? `/${ ty . pattern . source }/${ ty . pattern . flags }` : '(pattern)' }` : '?' ) ] , [ 'minLength' , escapeString ( ty . kind === 'primitive' ? `${ nvl ( ty . minLength , '0' ) }` : '?' ) ] , [ 'maxLength' , escapeString ( ty . kind === 'primitive' ? `${ nvl ( ty . maxLength , '(biggest)' ) }` : '?' ) ] , [ 'name' , escapeString ( `${ ty . kind !== 'repeated' && values . dataPath . endsWith ( 'repeated)' ) ? 'repeated item of ' : ty . kind !== 'sequence' && values . dataPath . endsWith ( 'sequence)' ) ? 'sequence item of ' : '' }${ values . entryName || '?' }` ) ] , [ 'parentType' , escapeString ( values . parentType || '?' ) ] , [ 'dataPath' , values . dataPath ] , ... ( args . substitutions || [ ] ) , ] ) ; for ( const ent of dict . entries ( ) ) { ret = ret . replace ( new RegExp ( `%{${ ent [ 0 ] }}` ) , ent [ 1 ] ) ; } return ret ; } interface DataPathEntry { name : string ; kind : 'type' | 'key' | 'index' ; } export function reportError ( errType , data , ty , args ) { const messages = [ ] ; if ( ty . messages ) { messages . push ( ty . messages ) ; } if ( args . ctx . errorMessages ) { messages . push ( args . ctx . errorMessages ) ; } messages . push ( defaultMessages ) ; const dataPathEntryArray = [ ] ; for ( let i = 0 ; i < args . ctx . typeStack . length ; i ++ ) { const p = args . ctx . typeStack [ i ] ; const next = args . ctx . typeStack [ i + 1 ] ; const pt = Array . isArray ( p ) ? p [ 0 ] : p ; const pi = Array . isArray ( next ) ? next [ 1 ] : void 0 ; let isSet = false ; if ( pt . kind === 'repeated' ) { if ( i !== args . ctx . typeStack . length - 1 ) { if ( pt . name ) { dataPathEntryArray . push ( { kind : 'key' , name : pt . name } ) ; } dataPathEntryArray . push ( { kind : 'index' , name : `(${ pi !== void 0 ? `${ pi }:` : '' }repeated)` } ) ; isSet = true ; } } else if ( pt . kind === 'sequence' ) { if ( i !== args . ctx . typeStack . length - 1 ) { if ( pt . name ) { dataPathEntryArray . push ( { kind : 'key' , name : pt . name } ) ; } dataPathEntryArray . push ( { kind : 'index' , name : `(${ pi !== void 0 ? `${ pi }:` : '' }sequence)` } ) ; isSet = true ; } } if ( ! isSet ) { if ( pt . name ) { if ( i === 0 ) { if ( pt . typeName ) { dataPathEntryArray . push ( { kind : 'type' , name : pt . typeName } ) ; } else { dataPathEntryArray . push ( { kind : 'key' , name : pt . name } ) ; } } else { const len = dataPathEntryArray . length ; if ( len && dataPathEntryArray [ len - 1 ] . kind === 'type' ) { if ( pt . kind === 'object' && next && pt . typeName ) { dataPathEntryArray . push ( { kind : 'type' , name : pt . typeName } ) ; } else { dataPathEntryArray . push ( { kind : 'key' , name : pt . name as string } ) ; } } else { if ( pt . typeName ) { dataPathEntryArray . push ( { kind : 'type' , name : pt . typeName } ) ; } else { dataPathEntryArray . push ( { kind : 'key' , name : pt . name } ) ; } } } } else if ( pt . typeName ) { dataPathEntryArray . push ( { kind : 'type' , name : pt . typeName } ) ; } } } let dataPath = '' ; for ( let i = 0 ; i < dataPathEntryArray . length ; i ++ ) { const p = dataPathEntryArray [ i ] ; dataPath += p . name ; if ( i + 1 === dataPathEntryArray . length ) { break ; } dataPath += p . kind === 'type' ? ':' : '.' ; } let parentType = '' ; let entryName = '' ; for ( let i = dataPathEntryArray . length - 1 ; 0 <= i ; i -- ) { const p = dataPathEntryArray [ i ] ; if ( p . kind === 'type' ) { if ( i !== 0 && i === dataPathEntryArray . length - 1 ) { const q = dataPathEntryArray [ i - 1 ] ; if ( q . kind === 'index' ) { continue ; } } parentType = p . name ; for ( let j = i + 1 ; j < dataPathEntryArray . length ; j ++ ) { const q = dataPathEntryArray [ j ] ; if ( q . kind === 'key' ) { entryName = q . name ; break ; } } break ; } } if ( ! parentType ) { for ( let i = args . ctx . typeStack . length - 1 ; 0 <= i ; i -- ) { const p = args . ctx . typeStack [ i ] ; const pt = Array . isArray ( p ) ? p [ 0 ] : p ; if ( pt . typeName ) { parentType = pt . typeName ; } } } const topRepeatable = findTopRepeatableAssertion ( args . ctx ) ; const values = { dataPath , topRepeatable , parentType , entryName } ; const constraints = { } ; const cSrces = [ ty as any ] ; if ( errType === ErrorTypes . RepeatQtyUnmatched && topRepeatable ) { cSrces . unshift ( topRepeatable as any ) ; } for ( const cSrc of cSrces ) { if ( nvl ( cSrc . minValue , false ) ) { constraints . minValue = cSrc . minValue ; } if ( nvl ( cSrc . maxValue , false ) ) { constraints . maxValue = cSrc . maxValue ; } if ( nvl ( cSrc . greaterThanValue , false ) ) { constraints . greaterThanValue = cSrc . greaterThanValue ; } if ( nvl ( cSrc . lessThanValue , false ) ) { constraints . lessThanValue = cSrc . lessThanValue ; } if ( nvl ( cSrc . minLength , false ) ) { constraints . minLength = cSrc . minLength ; } if ( nvl ( cSrc . maxLength , false ) ) { constraints . maxLength = cSrc . maxLength ; } if ( nvl ( cSrc . pattern , false ) ) { const pat = cSrc . pattern as any as RegExp ; constraints . pattern = `/${ pat . source }/${ pat . flags }` ; } if ( nvl ( cSrc . min , false ) ) { constraints . min = cSrc . min ; } if ( nvl ( cSrc . max , false ) ) { constraints . max = cSrc . max ; } } const val = { } ; switch ( typeof data ) { case 'number' : case 'bigint' : case 'string' : case 'boolean' : case 'undefined' : val . value = data ; break ; case 'object' : if ( data === null ) { val . value = data ; } } if ( ty . messageId ) { args . ctx . errors . push ( { code : `${ ty . messageId }-${ errorTypeNames [ errType ] }` , message : formatErrorMessage ( ty . message ? ty . message : getErrorMessage ( errType , ... messages ) , data , ty , args , values ) , dataPath , constraints , ... val , } ) ; } else if ( ty . message ) { args . ctx . errors . push ( { code : `${ errorTypeNames [ errType ] }` , message : formatErrorMessage ( ty . message , data , ty , args , values ) , dataPath , constraints , ... val , } ) ; } else { args . ctx . errors . push ( { code : `${ errorTypeNames [ errType ] }` , message : formatErrorMessage ( getErrorMessage ( errType , ... messages ) , data , ty , args , values ) , dataPath , constraints , ... val , } ) ; } } export function reportErrorWithPush ( errType , data , tyidx , args ) { try { args . ctx . typeStack . push ( tyidx ) ; reportError ( errType , data , tyidx [ 0 ] , args ) ; } finally { args . ctx . typeStack . pop ( ) ; } }