import * as t from 'io-ts' ; import { IntersectionType , TaggedUnionType , UnionType , ValidationError , } from 'io-ts' ; import { absurd , flow , identity , not , pipe } from 'fp-ts/function' ; import * as ReadonlyArray from 'fp-ts/ReadonlyArray' ; import * as Option from 'fp-ts/Option' ; import * as Either from 'fp-ts/Either' ; import * as SemiGroup from 'fp-ts/Semigroup' ; import arrayToPath from './arrayToPath' ; import * as ReadonlyRecord from 'fp-ts/ReadonlyRecord' ; import { ErrorObject , FieldErrorWithPath } from './types' ; const formatErrorPath = ( context ) : string => pipe ( context , ReadonlyArray . filterMapWithIndex ( ( index , contextEntry ) => { const previousIndex = index - 1 ; const shouldBeFiltered = context [ previousIndex ] === undefined || context [ previousIndex ] . type instanceof TaggedUnionType || context [ previousIndex ] . type instanceof UnionType || context [ previousIndex ] . type instanceof IntersectionType ; return shouldBeFiltered ? Option . none : Option . some ( contextEntry ) ; } ) , ReadonlyArray . map ( ( { key } ) => key ) , ReadonlyArray . map ( ( key ) => pipe ( key , ( k ) => parseInt ( k , 10 ) , Either . fromPredicate ( not < number > ( Number . isNaN ) , ( ) => key ) ) ) , ReadonlyArray . toArray , arrayToPath ) ; const formatError = ( e ) : FieldErrorWithPath => { const path = formatErrorPath ( e . context ) ; const message = pipe ( e . message , Either . fromNullable ( e . context ) , Either . mapLeft ( flow ( ReadonlyArray . last , Option . map ( ( contextEntry ) => `expected ${ contextEntry . type . name } but got ${ JSON . stringify ( contextEntry . actual ) }` ) , Option . getOrElseW ( ( ) => absurd < string > ( 'Error context is missing name' as never ) ) ) ) , Either . getOrElseW ( identity ) ) ; const type = pipe ( e . context , ReadonlyArray . last , Option . map ( ( contextEntry ) => contextEntry . type . name ) , Option . getOrElse ( ( ) => 'unknown' ) ) ; return { message , type , path } ; } ; const getObjectSemigroup = < A extends Record < string , unknown > = never > ( ) : SemiGroup . Semigroup < A > => ( { concat : ( first , second ) => Object . assign ( { } , second , first ) , } ) ; const concatToSingleError = ( errors ) : ErrorObject => pipe ( errors , ReadonlyArray . map ( ( error ) => ( { [ error . path ] : { type : error . type , message : error . message , } , } ) ) , ( errors ) => SemiGroup . fold ( getObjectSemigroup < ErrorObject > ( ) ) ( { } , errors ) ) ; const appendSeveralErrors = { concat : ( a , b ) => ( { ... b , types : { ... a . types , [ a . type ] : a . message , [ b . type ] : b . message } , } ) , } ; const concatToMultipleErrors = ( errors ) : ErrorObject => pipe ( ReadonlyRecord . fromFoldableMap ( appendSeveralErrors , ReadonlyArray . Foldable ) ( errors , ( error ) => [ error . path , error ] ) , ReadonlyRecord . map ( ( errorWithPath ) => { const { path , ... error } = errorWithPath ; return error ; } ) ) ; const errorsToRecord = ( validateAllFieldCriteria ) => ( validationErrors ) : ErrorObject => { const concat = validateAllFieldCriteria ? concatToMultipleErrors : concatToSingleError ; return pipe ( validationErrors , ReadonlyArray . map ( formatError ) , concat ) ; } ; export default errorsToRecord ;