import { Ajv } from 'ajv' ; import ajv = require ( 'ajv' ) ; import * as cloneDeep from 'lodash.clonedeep' ; import * as _get from 'lodash.get' ; import { createRequestAjv } from '../../framework/ajv' ; import { OpenAPIV3 , SerDesMap , Options , ValidateResponseOpts , } from '../../framework/types' ; interface TraversalStates { req : TraversalState ; res : TraversalState ; } interface TraversalState { discriminator : object ; kind : 'req' | 'res' ; path : string [ ] ; } interface TopLevelPathNodes { requestBodies : Root < SchemaObject > [ ] ; responses : Root < SchemaObject > [ ] ; } interface TopLevelSchemaNodes extends TopLevelPathNodes { schemas : Root < SchemaObject > [ ] ; requestBodies : Root < SchemaObject > [ ] ; responses : Root < SchemaObject > [ ] ; } class Node < T , P > { public readonly path : string [ ] ; public readonly parent : P ; public readonly schema : T ; constructor ( parent , schema , path ) { this . path = path ; this . parent = parent ; this . schema = schema ; } } type SchemaObjectNode = Node < SchemaObject , SchemaObject > ; class Root < T > extends Node < T , T > { constructor ( schema , path ) { super ( null , schema , path ) ; } } type SchemaObject = OpenAPIV3 . SchemaObject ; type ReferenceObject = OpenAPIV3 . ReferenceObject ; type Schema = ReferenceObject | SchemaObject ; if ( ! Array . prototype [ 'flatMap' ] ) { Array . prototype [ 'flatMap' ] = function ( lambda ) { return Array . prototype . concat . apply ( [ ] , this . map ( lambda ) ) ; } ; Object . defineProperty ( Array . prototype , 'flatMap' , { enumerable : false } ) ; } export const httpMethods = new Set ( [ 'get' , 'put' , 'post' , 'delete' , 'options' , 'head' , 'patch' , 'trace' , ] ) ; export class SchemaPreprocessor { private ajv : Ajv ; private apiDoc : OpenAPIV3 . Document ; private apiDocRes : OpenAPIV3 . Document ; private serDesMap : SerDesMap ; private responseOpts : ValidateResponseOpts ; constructor ( apiDoc , ajvOptions , validateResponsesOpts ) { this . ajv = createRequestAjv ( apiDoc , ajvOptions ) ; this . apiDoc = apiDoc ; this . serDesMap = ajvOptions . serDesMap ; this . responseOpts = validateResponsesOpts ; } public preProcess ( ) { const componentSchemas = this . gatherComponentSchemaNodes ( ) ; const r = this . gatherSchemaNodesFromPaths ( ) ; this . apiDocRes = ! ! this . responseOpts ? cloneDeep ( this . apiDoc ) : null ; const schemaNodes = { schemas : componentSchemas , requestBodies : r . requestBodies , responses : r . responses , } ; this . traverseSchemas ( schemaNodes , ( parent , schema , opts ) => this . schemaVisitor ( parent , schema , opts ) ) ; return { apiDoc : this . apiDoc , apiDocRes : this . apiDocRes , } ; } private gatherComponentSchemaNodes ( ) { const nodes = [ ] ; const componentSchemaMap = this . apiDoc ?. components ?. schemas ?? [ ] ; for ( const [ id , s ] of Object . entries ( componentSchemaMap ) ) { const schema = this . resolveSchema < SchemaObject > ( s ) ; this . apiDoc . components . schemas [ id ] = schema ; const path = [ 'components' , 'schemas' , id ] ; const node = new Root ( schema , path ) ; nodes . push ( node ) ; } return nodes ; } private gatherSchemaNodesFromPaths ( ) { const requestBodySchemas = [ ] ; const responseSchemas = [ ] ; for ( const [ p , pi ] of Object . entries ( this . apiDoc . paths ) ) { const pathItem = this . resolveSchema < OpenAPIV3 . PathItemObject > ( pi ) ; for ( const method of Object . keys ( pathItem ) ) { if ( httpMethods . has ( method ) ) { const operation = < OpenAPIV3 . OperationObject > pathItem [ method ] ; this . preprocessPathLevelParameters ( method , pathItem ) ; const path = [ 'paths' , p , method ] ; const node = new Root < OpenAPIV3 . OperationObject > ( operation , path ) ; const requestBodies = this . extractRequestBodySchemaNodes ( node ) ; const responseBodies = this . extractResponseSchemaNodes ( node ) ; requestBodySchemas . push ( ... requestBodies ) ; responseSchemas . push ( ... responseBodies ) ; } } } return { requestBodies : requestBodySchemas , responses : responseSchemas , } ; } param nodes param visit private traverseSchemas ( nodes , visit ) { const seen = new Set ( ) ; const recurse = ( parent , node , opts ) => { const schema = node . schema ; if ( ! schema || seen . has ( schema ) ) return ; seen . add ( schema ) ; if ( schema . $ref ) { const resolvedSchema = this . resolveSchema < SchemaObject > ( schema ) ; const path = schema . $ref . split ( '/' ) . slice ( 1 ) ; ( < any > opts ) . req . originalSchema = schema ; ( < any > opts ) . res . originalSchema = schema ; visit ( parent , node , opts ) ; recurse ( node , new Node ( schema , resolvedSchema , path ) , opts ) ; return ; } ( < any > opts ) . req . originalSchema = schema ; ( < any > opts ) . res . originalSchema = schema ; visit ( parent , node , opts ) ; if ( schema . allOf ) { schema . allOf . forEach ( ( s , i ) => { const child = new Node ( node , s , [ ... node . path , 'allOf' , i + '' ] ) ; recurse ( node , child , opts ) ; } ) ; } else if ( schema . oneOf ) { schema . oneOf . forEach ( ( s , i ) => { const child = new Node ( node , s , [ ... node . path , 'oneOf' , i + '' ] ) ; recurse ( node , child , opts ) ; } ) ; } else if ( schema . anyOf ) { schema . anyOf . forEach ( ( s , i ) => { const child = new Node ( node , s , [ ... node . path , 'anyOf' , i + '' ] ) ; recurse ( node , child , opts ) ; } ) ; } else if ( schema . properties ) { Object . entries ( schema . properties ) . forEach ( ( [ id , cschema ] ) => { const path = [ ... node . path , 'properties' , id ] ; const child = new Node ( node , cschema , path ) ; recurse ( node , child , opts ) ; } ) ; } } ; const initOpts = ( ) : TraversalStates => ( { req : { discriminator : { } , kind : 'req' , path : [ ] } , res : { discriminator : { } , kind : 'res' , path : [ ] } , } ) ; for ( const node of nodes . schemas ) { recurse ( null , node , initOpts ( ) ) ; } for ( const node of nodes . requestBodies ) { recurse ( null , node , initOpts ( ) ) ; } for ( const node of nodes . responses ) { recurse ( null , node , initOpts ( ) ) ; } } private schemaVisitor ( parent , node , opts ) { const pschemas = [ parent ?. schema ] ; const nschemas = [ node . schema ] ; if ( this . apiDocRes ) { const p = _get ( this . apiDocRes , parent ?. path ) ; const n = _get ( this . apiDocRes , node ?. path ) ; pschemas . push ( p ) ; nschemas . push ( n ) ; } for ( let i = 0 ; i < nschemas . length ; i ++ ) { const kind = i === 0 ? 'req' : 'res' ; const pschema = pschemas [ i ] ; const nschema = nschemas [ i ] ; const options = opts [ kind ] ; options . path = node . path ; if ( nschema ) { this . handleSerDes ( pschema , nschema , options ) ; this . handleReadonly ( pschema , nschema , options ) ; this . processDiscriminator ( pschema , nschema , options ) ; } } } private processDiscriminator ( parent , schema , opts = { } ) { const o = opts . discriminator ; const schemaObj = < SchemaObject > schema ; const xOf = schemaObj . oneOf ? 'oneOf' : schemaObj . anyOf ? 'anyOf' : null ; if ( xOf && schemaObj ?. discriminator ?. propertyName && ! o . discriminator ) { const options = schemaObj [ xOf ] . flatMap ( ( refObject ) => { if ( refObject [ '$ref' ] === undefined ) { return [ ] ; } const keys = this . findKeys ( schemaObj . discriminator . mapping , ( value ) => value === refObject [ '$ref' ] ) ; const ref = this . getKeyFromRef ( refObject [ '$ref' ] ) ; return keys . length > 0 ? keys . map ( ( option ) => ( { option , ref } ) ) : [ { option : ref , ref } ] ; } ) ; o . options = options ; o . discriminator = schemaObj . discriminator ?. propertyName ; o . properties = { ... ( o . properties ?? { } ) , ... ( schemaObj . properties ?? { } ) , } ; o . required = Array . from ( new Set ( ( o . required ?? [ ] ) . concat ( schemaObj . required ?? [ ] ) ) ) ; } if ( xOf ) return ; if ( o . discriminator ) { o . properties = { ... ( o . properties ?? { } ) , ... ( schemaObj . properties ?? { } ) , } ; o . required = Array . from ( new Set ( ( o . required ?? [ ] ) . concat ( schemaObj . required ?? [ ] ) ) ) ; const ancestor = parent ; const ref = opts . originalSchema . $ref ; if ( ! ref ) return ; const options = this . findKeys ( ancestor . discriminator ?. mapping , ( value ) => value === ref ) ; const refName = this . getKeyFromRef ( ref ) ; if ( options . length === 0 && ref ) { options . push ( refName ) ; } if ( options . length > 0 ) { const newSchema = JSON . parse ( JSON . stringify ( schemaObj ) ) ; const newProperties = { ... ( o . properties ?? { } ) , ... ( newSchema . properties ?? { } ) , } ; if ( Object . keys ( newProperties ) . length > 0 ) { newSchema . properties = newProperties ; } newSchema . required = o . required ; if ( newSchema . required . length === 0 ) { delete newSchema . required ; } ancestor . _discriminator ??= { validators : { } , options : o . options , property : o . discriminator , } ; for ( const option of options ) { ancestor . _discriminator . validators [ option ] = this . ajv . compile ( newSchema ) ; } } o . properties = { } ; delete o . required ; } } private handleSerDes ( parent , schema , state ) { if ( schema . type === 'string' && ! ! schema . format && this . serDesMap [ schema . format ] ) { ( < any > schema ) . type = [ this . serDesMap [ schema . format ] . jsonType || 'object' , 'string' ] ; schema [ 'x-eov-serdes' ] = this . serDesMap [ schema . format ] ; } } private handleReadonly ( parent , schema , opts ) { if ( opts . kind === 'res' ) return ; const required = parent ?. required ?? [ ] ; const prop = opts ?. path ?. [ opts ?. path ?. length - 1 ] ; const index = required . indexOf ( prop ) ; if ( schema . readOnly && index > - 1 ) { parent . required = required . slice ( 0 , index ) . concat ( required . slice ( index + 1 ) ) ; if ( parent . required . length === 0 ) { delete parent . required ; } } } param op private extractRequestBodySchemaNodes ( node ) { const op = node . schema ; const bodySchema = this . resolveSchema < OpenAPIV3 . RequestBodyObject > ( op . requestBody ) ; op . requestBody = bodySchema ; if ( ! bodySchema ?. content ) return [ ] ; const result = [ ] ; const contentEntries = Object . entries ( bodySchema . content ) ; for ( const [ type , mediaTypeObject ] of contentEntries ) { const mediaTypeSchema = this . resolveSchema < SchemaObject > ( mediaTypeObject . schema ) ; op . requestBody . content [ type ] . schema = mediaTypeSchema ; const path = [ ... node . path , 'requestBody' , 'content' , type , 'schema' ] ; result . push ( new Root ( mediaTypeSchema , path ) ) ; } return result ; } private extractResponseSchemaNodes ( node ) { const op = node . schema ; const responses = op . responses ; if ( ! responses ) return ; const schemas = [ ] ; for ( const [ statusCode , response ] of Object . entries ( responses ) ) { const rschema = this . resolveSchema < OpenAPIV3 . ResponseObject > ( response ) ; if ( ! rschema ) { continue ; } responses [ statusCode ] = rschema ; if ( rschema . content ) { for ( const [ type , mediaType ] of Object . entries ( rschema . content ) ) { const schema = this . resolveSchema < SchemaObject > ( mediaType ?. schema ) ; if ( schema ) { rschema . content [ type ] . schema = schema ; const path = [ ... node . path , 'responses' , statusCode , 'content' , type , 'schema' , ] ; schemas . push ( new Root ( schema , path ) ) ; } } } } return schemas ; } private resolveSchema < T > ( schema ) { if ( ! schema ) return null ; const ref = schema ?. [ '$ref' ] ; let res = ( ref ? this . ajv . getSchema ( ref ) ?. schema : schema ) as T ; if ( ref && ! res ) { const path = ref . split ( '/' ) . join ( '.' ) ; const p = path . substring ( path . indexOf ( '.' ) + 1 ) ; res = _get ( this . apiDoc , p ) ; } return res ; } param pathItemKey param pathItem private preprocessPathLevelParameters ( pathItemKey , pathItem ) { const parameters = pathItem . parameters ?? [ ] ; if ( parameters . length === 0 ) return ; const v = this . resolveSchema < OpenAPIV3 . OperationObject > ( pathItem [ pathItemKey ] ) ; if ( v === parameters ) return ; v . parameters = v . parameters || [ ] ; const match = ( pathParam , opParam ) => ( opParam [ 'name' ] && opParam [ 'name' ] === pathParam [ 'name' ] ) || ( opParam [ '$ref' ] && opParam [ '$ref' ] === pathParam [ '$ref' ] ) ; for ( const param of parameters ) { if ( ! v . parameters . some ( ( vparam ) => match ( param , vparam ) ) ) { v . parameters . push ( param ) ; } } } private findKeys ( object , searchFunc ) { const matches = [ ] ; if ( ! object ) { return matches ; } const keys = Object . keys ( object ) ; for ( let i = 0 ; i < keys . length ; i ++ ) { if ( searchFunc ( object [ keys [ i ] ] ) ) { matches . push ( keys [ i ] ) ; } } return matches ; } getKeyFromRef ( ref ) { return ref . split ( '/components/schemas/' ) [ 1 ] ; } }