You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					172 lines
				
				5.2 KiB
			
		
		
			
		
	
	
					172 lines
				
				5.2 KiB
			| 
											3 years ago
										 | import type {KeywordCxt} from "." | ||
|  | import type { | ||
|  |   AnySchema, | ||
|  |   SchemaValidateFunction, | ||
|  |   AnyValidateFunction, | ||
|  |   AddedKeywordDefinition, | ||
|  |   MacroKeywordDefinition, | ||
|  |   FuncKeywordDefinition, | ||
|  | } from "../../types" | ||
|  | import type {SchemaObjCxt} from ".." | ||
|  | import {_, nil, not, stringify, Code, Name, CodeGen} from "../codegen" | ||
|  | import N from "../names" | ||
|  | import type {JSONType} from "../rules" | ||
|  | import {callValidateCode} from "../../vocabularies/code" | ||
|  | import {extendErrors} from "../errors" | ||
|  | 
 | ||
|  | type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction | ||
|  | 
 | ||
|  | export function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void { | ||
|  |   const {gen, keyword, schema, parentSchema, it} = cxt | ||
|  |   const macroSchema = def.macro.call(it.self, schema, parentSchema, it) | ||
|  |   const schemaRef = useKeyword(gen, keyword, macroSchema) | ||
|  |   if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) | ||
|  | 
 | ||
|  |   const valid = gen.name("valid") | ||
|  |   cxt.subschema( | ||
|  |     { | ||
|  |       schema: macroSchema, | ||
|  |       schemaPath: nil, | ||
|  |       errSchemaPath: `${it.errSchemaPath}/${keyword}`, | ||
|  |       topSchemaRef: schemaRef, | ||
|  |       compositeRule: true, | ||
|  |     }, | ||
|  |     valid | ||
|  |   ) | ||
|  |   cxt.pass(valid, () => cxt.error(true)) | ||
|  | } | ||
|  | 
 | ||
|  | export function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void { | ||
|  |   const {gen, keyword, schema, parentSchema, $data, it} = cxt | ||
|  |   checkAsyncKeyword(it, def) | ||
|  |   const validate = | ||
|  |     !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate | ||
|  |   const validateRef = useKeyword(gen, keyword, validate) | ||
|  |   const valid = gen.let("valid") | ||
|  |   cxt.block$data(valid, validateKeyword) | ||
|  |   cxt.ok(def.valid ?? valid) | ||
|  | 
 | ||
|  |   function validateKeyword(): void { | ||
|  |     if (def.errors === false) { | ||
|  |       assignValid() | ||
|  |       if (def.modifying) modifyData(cxt) | ||
|  |       reportErrs(() => cxt.error()) | ||
|  |     } else { | ||
|  |       const ruleErrs = def.async ? validateAsync() : validateSync() | ||
|  |       if (def.modifying) modifyData(cxt) | ||
|  |       reportErrs(() => addErrs(cxt, ruleErrs)) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function validateAsync(): Name { | ||
|  |     const ruleErrs = gen.let("ruleErrs", null) | ||
|  |     gen.try( | ||
|  |       () => assignValid(_`await `), | ||
|  |       (e) => | ||
|  |         gen.assign(valid, false).if( | ||
|  |           _`${e} instanceof ${it.ValidationError as Name}`, | ||
|  |           () => gen.assign(ruleErrs, _`${e}.errors`), | ||
|  |           () => gen.throw(e) | ||
|  |         ) | ||
|  |     ) | ||
|  |     return ruleErrs | ||
|  |   } | ||
|  | 
 | ||
|  |   function validateSync(): Code { | ||
|  |     const validateErrs = _`${validateRef}.errors` | ||
|  |     gen.assign(validateErrs, null) | ||
|  |     assignValid(nil) | ||
|  |     return validateErrs | ||
|  |   } | ||
|  | 
 | ||
|  |   function assignValid(_await: Code = def.async ? _`await ` : nil): void { | ||
|  |     const passCxt = it.opts.passContext ? N.this : N.self | ||
|  |     const passSchema = !(("compile" in def && !$data) || def.schema === false) | ||
|  |     gen.assign( | ||
|  |       valid, | ||
|  |       _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`, | ||
|  |       def.modifying | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   function reportErrs(errors: () => void): void { | ||
|  |     gen.if(not(def.valid ?? valid), errors) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function modifyData(cxt: KeywordCxt): void { | ||
|  |   const {gen, data, it} = cxt | ||
|  |   gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) | ||
|  | } | ||
|  | 
 | ||
|  | function addErrs(cxt: KeywordCxt, errs: Code): void { | ||
|  |   const {gen} = cxt | ||
|  |   gen.if( | ||
|  |     _`Array.isArray(${errs})`, | ||
|  |     () => { | ||
|  |       gen | ||
|  |         .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) | ||
|  |         .assign(N.errors, _`${N.vErrors}.length`) | ||
|  |       extendErrors(cxt) | ||
|  |     }, | ||
|  |     () => cxt.error() | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function checkAsyncKeyword({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void { | ||
|  |   if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema") | ||
|  | } | ||
|  | 
 | ||
|  | function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name { | ||
|  |   if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`) | ||
|  |   return gen.scopeValue( | ||
|  |     "keyword", | ||
|  |     typeof result == "function" ? {ref: result} : {ref: result, code: stringify(result)} | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | export function validSchemaType( | ||
|  |   schema: unknown, | ||
|  |   schemaType: JSONType[], | ||
|  |   allowUndefined = false | ||
|  | ): boolean { | ||
|  |   // TODO add tests
 | ||
|  |   return ( | ||
|  |     !schemaType.length || | ||
|  |     schemaType.some((st) => | ||
|  |       st === "array" | ||
|  |         ? Array.isArray(schema) | ||
|  |         : st === "object" | ||
|  |         ? schema && typeof schema == "object" && !Array.isArray(schema) | ||
|  |         : typeof schema == st || (allowUndefined && typeof schema == "undefined") | ||
|  |     ) | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | export function validateKeywordUsage( | ||
|  |   {schema, opts, self, errSchemaPath}: SchemaObjCxt, | ||
|  |   def: AddedKeywordDefinition, | ||
|  |   keyword: string | ||
|  | ): void { | ||
|  |   /* istanbul ignore if */ | ||
|  |   if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { | ||
|  |     throw new Error("ajv implementation error") | ||
|  |   } | ||
|  | 
 | ||
|  |   const deps = def.dependencies | ||
|  |   if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { | ||
|  |     throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (def.validateSchema) { | ||
|  |     const valid = def.validateSchema(schema[keyword]) | ||
|  |     if (!valid) { | ||
|  |       const msg = | ||
|  |         `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` + | ||
|  |         self.errorsText(def.validateSchema.errors) | ||
|  |       if (opts.validateSchema === "log") self.logger.error(msg) | ||
|  |       else throw new Error(msg) | ||
|  |     } | ||
|  |   } | ||
|  | } |