import type { AnySchema , AnySchemaObject , AnyValidateFunction , AsyncValidateFunction , EvaluatedProperties , EvaluatedItems , } from "../types" ; import type Ajv from "../core" ; import type { InstanceOptions } from "../core" ; import { CodeGen , _ , nil , stringify , Name , Code , ValueScopeName } from "./codegen" ; import ValidationError from "../runtime/validation_error" ; import N from "./names" ; import { LocalRefs , getFullPath , _getFullPath , inlineRef , normalizeId , resolveUrl } from "./resolve" ; import { schemaHasRulesButRef , unescapeFragment } from "./util" ; import { validateFunctionCode } from "./validate" ; import * as URI from "uri-js" ; import { JSONType } from "./rules" ; export type SchemaRefs = { [ Ref in string ] ? : SchemaEnv | AnySchema ; } ; export interface SchemaCxt { readonly gen : CodeGen ; readonly allErrors ? : boolean ; readonly data : Name ; readonly parentData : Name ; readonly parentDataProperty : Code | number ; readonly dataNames : Name [ ] ; readonly dataPathArr : ( Code | number ) [ ] ; readonly dataLevel : number ; dataTypes : JSONType [ ] ; definedProperties : Set < string > ; readonly topSchemaRef : Code ; readonly validateName : Name ; evaluated ? : Name ; readonly ValidationError ? : Name ; readonly schema : AnySchema ; readonly schemaEnv : SchemaEnv ; readonly rootId : string ; baseId : string ; readonly schemaPath : Code ; readonly errSchemaPath : string ; readonly errorPath : Code ; readonly propertyName ? : Name ; readonly compositeRule ? : boolean ; props ? : EvaluatedProperties | Name ; items ? : EvaluatedItems | Name ; jtdDiscriminator ? : string ; jtdMetadata ? : boolean ; readonly createErrors ? : boolean ; readonly opts : InstanceOptions ; readonly self : Ajv ; } export interface SchemaObjCxt extends SchemaCxt { readonly schema : AnySchemaObject ; } interface SchemaEnvArgs { readonly schema : AnySchema ; readonly schemaId ? : "$id" | "id" ; readonly root ? : SchemaEnv ; readonly baseId ? : string ; readonly schemaPath ? : string ; readonly localRefs ? : LocalRefs ; readonly meta ? : boolean ; } export class SchemaEnv implements SchemaEnvArgs { readonly schema : AnySchema ; readonly schemaId ? : "$id" | "id" ; readonly root : SchemaEnv ; baseId : string ; schemaPath ? : string ; localRefs ? : LocalRefs ; readonly meta ? : boolean ; readonly $async ? : boolean ; readonly refs : SchemaRefs = { } ; readonly dynamicAnchors : { [ Ref in string ] ? : true ; } = { } ; validate ? : AnyValidateFunction ; validateName ? : ValueScopeName ; serialize ? : ( data : unknown ) => string ; serializeName ? : ValueScopeName ; parse ? : ( data : string ) => unknown ; parseName ? : ValueScopeName ; constructor ( env ) { let schema ; if ( typeof env . schema == "object" ) schema = env . schema ; this . schema = env . schema ; this . schemaId = env . schemaId ; this . root = env . root || this ; this . baseId = env . baseId ?? normalizeId ( schema ?. [ env . schemaId || "$id" ] ) ; this . schemaPath = env . schemaPath ; this . localRefs = env . localRefs ; this . meta = env . meta ; this . $async = schema ?. $async ; this . refs = { } ; } } export function compileSchema ( this , sch ) { const _sch = getCompilingSchema . call ( this , sch ) ; if ( _sch ) return _sch ; const rootId = getFullPath ( sch . root . baseId ) ; const { es5 , lines } = this . opts . code ; const { ownProperties } = this . opts ; const gen = new CodeGen ( this . scope , { es5 , lines , ownProperties } ) ; let _ValidationError ; if ( sch . $async ) { _ValidationError = gen . scopeValue ( "Error" , { ref : ValidationError , code : _ `require("ajv/dist/runtime/validation_error").default` , } ) ; } const validateName = gen . scopeName ( "validate" ) ; sch . validateName = validateName ; const schemaCxt = { gen , allErrors : this . opts . allErrors , data : N . data , parentData : N . parentData , parentDataProperty : N . parentDataProperty , dataNames : [ N . data ] , dataPathArr : [ nil ] , dataLevel : 0 , dataTypes : [ ] , definedProperties : new Set < string > ( ) , topSchemaRef : gen . scopeValue ( "schema" , this . opts . code . source === true ? { ref : sch . schema , code : stringify ( sch . schema ) } : { ref : sch . schema } ) , validateName , ValidationError : _ValidationError , schema : sch . schema , schemaEnv : sch , rootId , baseId : sch . baseId || rootId , schemaPath : nil , errSchemaPath : sch . schemaPath || ( this . opts . jtd ? "" : "#" ) , errorPath : _ `""` , opts : this . opts , self : this , } ; let sourceCode ; try { this . _compilations . add ( sch ) ; validateFunctionCode ( schemaCxt ) ; gen . optimize ( this . opts . code . optimize ) ; const validateCode = gen . toString ( ) ; sourceCode = `${ gen . scopeRefs ( N . scope ) }return ${ validateCode }` ; if ( this . opts . code . process ) sourceCode = this . opts . code . process ( sourceCode , sch ) ; const makeValidate = new Function ( `${ N . self }` , `${ N . scope }` , sourceCode ) ; const validate = makeValidate ( this , this . scope . get ( ) ) ; this . scope . value ( validateName , { ref : validate } ) ; validate . errors = null ; validate . schema = sch . schema ; validate . schemaEnv = sch ; if ( sch . $async ) ( validate as AsyncValidateFunction ) . $async = true ; if ( this . opts . code . source === true ) { validate . source = { validateName , validateCode , scopeValues : gen . _values } ; } if ( this . opts . unevaluated ) { const { props , items } = schemaCxt ; validate . evaluated = { props : props instanceof Name ? undefined : props , items : items instanceof Name ? undefined : items , dynamicProps : props instanceof Name , dynamicItems : items instanceof Name , } ; if ( validate . source ) validate . source . evaluated = stringify ( validate . evaluated ) ; } sch . validate = validate ; return sch ; } catch ( e ) { delete sch . validate ; delete sch . validateName ; if ( sourceCode ) this . logger . error ( "Error compiling schema, function code:" , sourceCode ) ; throw e ; } finally { this . _compilations . delete ( sch ) ; } } export function resolveRef ( this , root , baseId , ref ) { ref = resolveUrl ( baseId , ref ) ; const schOrFunc = root . refs [ ref ] ; if ( schOrFunc ) return schOrFunc ; let _sch = resolve . call ( this , root , ref ) ; if ( _sch === undefined ) { const schema = root . localRefs ?. [ ref ] ; const { schemaId } = this . opts ; if ( schema ) _sch = new SchemaEnv ( { schema , schemaId , root , baseId } ) ; } if ( _sch === undefined ) return ; return ( root . refs [ ref ] = inlineOrCompile . call ( this , _sch ) ) ; } function inlineOrCompile ( this , sch ) { if ( inlineRef ( sch . schema , this . opts . inlineRefs ) ) return sch . schema ; return sch . validate ? sch : compileSchema . call ( this , sch ) ; } export function getCompilingSchema ( this , schEnv ) { for ( const sch of this . _compilations ) { if ( sameSchemaEnv ( sch , schEnv ) ) return sch ; } } function sameSchemaEnv ( s1 , s2 ) { return s1 . schema === s2 . schema && s1 . root === s2 . root && s1 . baseId === s2 . baseId ; } function resolve ( this , root , ref ) { let sch ; while ( typeof ( sch = this . refs [ ref ] ) == "string" ) ref = sch ; return sch || this . schemas [ ref ] || resolveSchema . call ( this , root , ref ) ; } export function resolveSchema ( this , root , ref ) { const p = URI . parse ( ref ) ; const refPath = _getFullPath ( p ) ; let baseId = getFullPath ( root . baseId ) ; if ( Object . keys ( root . schema ) . length > 0 && refPath === baseId ) { return getJsonPointer . call ( this , p , root ) ; } const id = normalizeId ( refPath ) ; const schOrRef = this . refs [ id ] || this . schemas [ id ] ; if ( typeof schOrRef == "string" ) { const sch = resolveSchema . call ( this , root , schOrRef ) ; if ( typeof sch ?. schema !== "object" ) return ; return getJsonPointer . call ( this , p , sch ) ; } if ( typeof schOrRef ?. schema !== "object" ) return ; if ( ! schOrRef . validate ) compileSchema . call ( this , schOrRef ) ; if ( id === normalizeId ( ref ) ) { const { schema } = schOrRef ; const { schemaId } = this . opts ; const schId = schema [ schemaId ] ; if ( schId ) baseId = resolveUrl ( baseId , schId ) ; return new SchemaEnv ( { schema , schemaId , root , baseId } ) ; } return getJsonPointer . call ( this , p , schOrRef ) ; } const PREVENT_SCOPE_CHANGE = new Set ( [ "properties" , "patternProperties" , "enum" , "dependencies" , "definitions" , ] ) ; function getJsonPointer ( this , parsedRef , { baseId , schema , root } ) { if ( parsedRef . fragment ?. [ 0 ] !== "/" ) return ; for ( const part of parsedRef . fragment . slice ( 1 ) . split ( "/" ) ) { if ( typeof schema === "boolean" ) return ; const partSchema = schema [ unescapeFragment ( part ) ] ; if ( partSchema === undefined ) return ; schema = partSchema ; const schId = typeof schema === "object" && schema [ this . opts . schemaId ] ; if ( ! PREVENT_SCOPE_CHANGE . has ( part ) && schId ) { baseId = resolveUrl ( baseId , schId ) ; } } let env ; if ( typeof schema != "boolean" && schema . $ref && ! schemaHasRulesButRef ( schema , this . RULES ) ) { const $ref = resolveUrl ( baseId , schema . $ref ) ; env = resolveSchema . call ( this , root , $ref ) ; } const { schemaId } = this . opts ; env = env || new SchemaEnv ( { schema , schemaId , root , baseId } ) ; if ( env . schema !== env . root . schema ) return env ; return undefined ; }