import { Validator , ValidationContext , ValidationResult , ObjectValidator , PropertyModel , MapEntryModel , isNullOrUndefined , defaultViolations , isString , Violation , TypeMismatch , ObjectModel , } from './validators' ; import { Path } from '@finnair/path' ; export interface DiscriminatorFn { ( value ) : string ; } export type Discriminator = string | DiscriminatorFn ; export interface SchemaModel { readonly discriminator : Discriminator ; readonly models : { [ name ] : Validator | ClassModel ; } ; } export type ClassParentModel = string | ObjectValidator | ( string | ObjectValidator ) [ ] ; export interface ClassModel { readonly properties ? : PropertyModel ; readonly additionalProperties ? : boolean | MapEntryModel | MapEntryModel [ ] ; readonly extends ? : ClassParentModel ; readonly localProperties ? : PropertyModel ; readonly next ? : Validator ; readonly localNext ? : Validator ; } export class ModelRef extends Validator { constructor ( private schema , public readonly name ) { super ( ) ; } validatePath ( value , path , ctx ) { return this . schema . validateClass ( value , path , ctx , this . name ) ; } } export class DiscriminatorViolation extends Violation { constructor ( path , value , public readonly expectedOneOf ) { super ( path , 'Discriminator' , value ) ; } } export class SchemaValidator extends Validator { private readonly discriminator : Discriminator ; private readonly proxies = new Map < string , Validator > ( ) ; private readonly validators : { [ name ] : Validator ; } = { } ; constructor ( fn ) { super ( ) ; const schema = fn ( this ) ; for ( const name of this . proxies . keys ( ) ) { if ( ! schema . models [ name ] ) { throw new Error ( 'Undefined named model: ' + name ) ; } } this . discriminator = schema . discriminator ; this . compileSchema ( schema . models , new Set < string > ( ) ) ; } async validatePath ( value , path , ctx ) { return this . validateClass ( value , path , ctx ) ; } async validateClass ( value , path , ctx , expectedType ? ) { if ( isNullOrUndefined ( value ) ) { return ctx . failure ( defaultViolations . notNull ( path ) , value ) ; } let type ; let typePath = path ; if ( isString ( this . discriminator ) ) { type = value [ this . discriminator as string ] ; typePath = path . property ( this . discriminator as string ) ; } else { type = ( this . discriminator as DiscriminatorFn ) ( value ) ; } const validator = this . validators [ type ] ; if ( ! validator ) { return ctx . failure ( new DiscriminatorViolation ( typePath , type , Object . keys ( this . validators ) ) , type ) ; } if ( expectedType ) { const expectedParent = this . validators [ expectedType ] ; if ( ! this . isSubtypeOf ( validator , expectedParent ) ) { return ctx . failure ( new TypeMismatch ( path , expectedType , type ) , type ) ; } } return validator . validatePath ( value , path , ctx ) ; } of ( name ) { if ( ! this . proxies . has ( name ) ) { if ( this . discriminator && ! this . validators [ name ] ) { throw new Error ( `Unknown model: ${ name }` ) ; } this . proxies . set ( name , new ModelRef ( this , name ) ) ; } return this . proxies . get ( name ) ! ; } raw ( name ) { if ( ! this . validators [ name ] ) { throw new Error ( `Validator not found: ${ name }` ) ; } return this . validators [ name ] ! ; } private isSubtypeOf ( validator , expectedParent ) { if ( validator === expectedParent ) { return true ; } if ( validator instanceof ObjectValidator ) { return validator . parentValidators . some ( parent => this . isSubtypeOf ( parent , expectedParent ) ) ; } return false ; } private compileSchema ( models , seen ) { for ( const name in models ) { this . compileClass ( name , models , seen ) ; } } private compileClass ( name , models , seen ) { if ( seen . has ( name ) ) { if ( this . validators [ name ] ) { return this . validators [ name ] ; } throw new Error ( `Cyclic dependency: ${ name }` ) ; } seen . add ( name ) ; let validator ; if ( models [ name ] instanceof Validator ) { validator = models [ name ] as Validator ; } else { const classModel = models [ name ] as ClassModel ; const localProperties = classModel . localProperties || { } ; if ( isString ( this . discriminator ) ) { const discriminatorProperty = this . discriminator as string ; if ( ! localProperties [ discriminatorProperty ] ) { localProperties [ discriminatorProperty ] = name ; } } const model = { extends : this . getParentValidators ( classModel . extends , models , seen ) , properties : classModel . properties , additionalProperties : classModel . additionalProperties , localProperties , next : classModel . next , localNext : classModel . localNext , } ; validator = new ObjectValidator ( model ) ; } this . validators [ name ] = validator ; return validator ; } getParentValidators ( parents , models , seen ) { let parentValidators = [ ] ; if ( parents ) { parentValidators = parentValidators . concat ( parents ) ; } return parentValidators . map ( ( nameOrValidator ) => { let parent ; if ( isString ( nameOrValidator ) ) { parent = this . compileClass ( nameOrValidator as string , models , seen ) ; } else { parent = nameOrValidator as Validator ; } if ( ! ( parent instanceof ObjectValidator ) ) { throw new Error ( `Illegal inheritance: objects may only inherit from other objects (ObjectValidators)` ) ; } return parent as ObjectValidator ; } ) ; } }