import type {AnySchema, EvaluatedProperties, EvaluatedItems} from "../types" import type {SchemaCxt, SchemaObjCxt} from "." import {_, getProperty, Code, Name, CodeGen} from "./codegen" import {_Code} from "./codegen/code" import type {Rule, ValidationRules} from "./rules" // TODO refactor to use Set export function toHash(arr: T[]): {[K in T]?: true} { const hash: {[K in T]?: true} = {} for (const item of arr) hash[item] = true return hash } export function alwaysValidSchema(it: SchemaCxt, schema: AnySchema): boolean | void { if (typeof schema == "boolean") return schema if (Object.keys(schema).length === 0) return true checkUnknownRules(it, schema) return !schemaHasRules(schema, it.self.RULES.all) } export function checkUnknownRules(it: SchemaCxt, schema: AnySchema = it.schema): void { const {opts, self} = it if (!opts.strictSchema) return if (typeof schema === "boolean") return const rules = self.RULES.keywords for (const key in schema) { if (!rules[key]) checkStrictMode(it, `unknown keyword: "${key}"`) } } export function schemaHasRules( schema: AnySchema, rules: {[Key in string]?: boolean | Rule} ): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true return false } export function schemaHasRulesButRef(schema: AnySchema, RULES: ValidationRules): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true return false } export function schemaRefOrVal( {topSchemaRef, schemaPath}: SchemaObjCxt, schema: unknown, keyword: string, $data?: string | false ): Code | number | boolean { if (!$data) { if (typeof schema == "number" || typeof schema == "boolean") return schema if (typeof schema == "string") return _`${schema}` } return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}` } export function unescapeFragment(str: string): string { return unescapeJsonPointer(decodeURIComponent(str)) } export function escapeFragment(str: string | number): string { return encodeURIComponent(escapeJsonPointer(str)) } export function escapeJsonPointer(str: string | number): string { if (typeof str == "number") return `${str}` return str.replace(/~/g, "~0").replace(/\//g, "~1") } export function unescapeJsonPointer(str: string): string { return str.replace(/~1/g, "/").replace(/~0/g, "~") } export function eachItem(xs: T | T[], f: (x: T) => void): void { if (Array.isArray(xs)) { for (const x of xs) f(x) } else { f(xs) } } type SomeEvaluated = EvaluatedProperties | EvaluatedItems type MergeEvaluatedFunc = ( gen: CodeGen, from: Name | T, to: Name | Exclude | undefined, toName?: typeof Name ) => Name | T interface MakeMergeFuncArgs { mergeNames: (gen: CodeGen, from: Name, to: Name) => void mergeToName: (gen: CodeGen, from: T, to: Name) => void mergeValues: (from: T, to: Exclude) => T resultToName: (gen: CodeGen, res?: T) => Name } function makeMergeEvaluated({ mergeNames, mergeToName, mergeValues, resultToName, }: MakeMergeFuncArgs): MergeEvaluatedFunc { return (gen, from, to, toName) => { const res = to === undefined ? from : to instanceof Name ? (from instanceof Name ? mergeNames(gen, from, to) : mergeToName(gen, from, to), to) : from instanceof Name ? (mergeToName(gen, to, from), from) : mergeValues(from, to) return toName === Name && !(res instanceof Name) ? resultToName(gen, res) : res } } interface MergeEvaluated { props: MergeEvaluatedFunc items: MergeEvaluatedFunc } export const mergeEvaluated: MergeEvaluated = { props: makeMergeEvaluated({ mergeNames: (gen, from, to) => gen.if(_`${to} !== true && ${from} !== undefined`, () => { gen.if( _`${from} === true`, () => gen.assign(to, true), () => gen.assign(to, _`${to} || {}`).code(_`Object.assign(${to}, ${from})`) ) }), mergeToName: (gen, from, to) => gen.if(_`${to} !== true`, () => { if (from === true) { gen.assign(to, true) } else { gen.assign(to, _`${to} || {}`) setEvaluated(gen, to, from) } }), mergeValues: (from, to) => (from === true ? true : {...from, ...to}), resultToName: evaluatedPropsToName, }), items: makeMergeEvaluated({ mergeNames: (gen, from, to) => gen.if(_`${to} !== true && ${from} !== undefined`, () => gen.assign(to, _`${from} === true ? true : ${to} > ${from} ? ${to} : ${from}`) ), mergeToName: (gen, from, to) => gen.if(_`${to} !== true`, () => gen.assign(to, from === true ? true : _`${to} > ${from} ? ${to} : ${from}`) ), mergeValues: (from, to) => (from === true ? true : Math.max(from, to)), resultToName: (gen, items) => gen.var("items", items), }), } export function evaluatedPropsToName(gen: CodeGen, ps?: EvaluatedProperties): Name { if (ps === true) return gen.var("props", true) const props = gen.var("props", _`{}`) if (ps !== undefined) setEvaluated(gen, props, ps) return props } export function setEvaluated(gen: CodeGen, props: Name, ps: {[K in string]?: true}): void { Object.keys(ps).forEach((p) => gen.assign(_`${props}${getProperty(p)}`, true)) } const snippets: {[S in string]?: _Code} = {} export function useFunc(gen: CodeGen, f: {code: string}): Name { return gen.scopeValue("func", { ref: f, code: snippets[f.code] || (snippets[f.code] = new _Code(f.code)), }) } export enum Type { Num, Str, } export function getErrorPath( dataProp: Name | string | number, dataPropType?: Type, jsPropertySyntax?: boolean ): Code | string { // let path if (dataProp instanceof Name) { const isNumber = dataPropType === Type.Num return jsPropertySyntax ? isNumber ? _`"[" + ${dataProp} + "]"` : _`"['" + ${dataProp} + "']"` : isNumber ? _`"/" + ${dataProp}` : _`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")` // TODO maybe use global escapePointer } return jsPropertySyntax ? getProperty(dataProp).toString() : "/" + escapeJsonPointer(dataProp) } export function checkStrictMode( it: SchemaCxt, msg: string, mode: boolean | "log" = it.opts.strictSchema ): void { if (!mode) return msg = `strict mode: ${msg}` if (mode === true) throw new Error(msg) it.self.logger.warn(msg) }