Skip to content

Commit

Permalink
Merge 656a315 into 04775b1
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Sep 18, 2020
2 parents 04775b1 + 656a315 commit 080ee7d
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 257 deletions.
558 changes: 363 additions & 195 deletions README.md

Large diffs are not rendered by default.

90 changes: 65 additions & 25 deletions lib/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface Plugin<Opts> {
import KeywordCxt from "./compile/context"
export {KeywordCxt}
export {DefinedError} from "./vocabularies/errors"
export {JSONSchemaType} from "./types/json-schema"

import type {
Schema,
Expand All @@ -36,6 +37,8 @@ import type {
KeywordDefinition,
Options,
InstanceOptions,
RemovedOptions,
DeprecatedOptions,
AnyValidateFunction,
ValidateFunction,
AsyncValidateFunction,
Expand Down Expand Up @@ -65,7 +68,7 @@ import draft7MetaSchema from "./refs/json-schema-draft-07.json"

const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema"

const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"]
const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"]
const META_SUPPORT_DATA = ["/properties"]
const EXT_SCOPE_NAMES = new Set([
"validate",
Expand All @@ -88,6 +91,28 @@ const optsDefaults = {
addUsedSchema: true,
}

type OptionsInfo<T extends RemovedOptions | DeprecatedOptions> = {
[key in keyof T]-?: string | undefined
}

const removedOptions: OptionsInfo<RemovedOptions> = {
errorDataPath: "",
format: "`validateFormats: false` can be used instead.",
nullable: '"nullable" keyword is supported by default.',
jsonPointers: "Deprecated jsPropertySyntax can be used instead.",
schemaId: "JSON Schema draft-04 is not supported in Ajv v7.",
strictDefaults: "It is default now, see option `strict`.",
strictKeywords: "It is default now, see option `strict`.",
strictNumbers: "It is default now, see option `strict`.",
uniqueItems: '"uniqueItems" keyword is always validated.',
unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).",
}

const deprecatedOptions: OptionsInfo<DeprecatedOptions> = {
jsPropertySyntax: "",
unicode: '"minLength"/"maxLength" account for unicode characters by default.',
}

export default class Ajv {
opts: InstanceOptions
errors?: ErrorObject[] | null // errors from the last validation
Expand All @@ -113,14 +138,16 @@ export default class Ajv {
serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify,
addUsedSchema: opts.addUsedSchema ?? true,
validateSchema: opts.validateSchema ?? true,
validateFormats: opts.validateFormats ?? true,
}
this.logger = getLogger(opts.logger)
const formatOpt = opts.format
opts.format = false
const formatOpt = opts.validateFormats
opts.validateFormats = false

this._cache = opts.cache || new Cache()
this.RULES = getRules()
checkDeprecatedOptions.call(this, opts)
checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED")
checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn")
this._metaOpts = getMetaSchemaOptions.call(this)

if (opts.formats) addInitialFormats.call(this)
Expand All @@ -135,18 +162,20 @@ export default class Ajv {
addDefaultMetaSchema.call(this)
if (typeof opts.meta == "object") this.addMetaSchema(opts.meta)
addInitialSchemas.call(this)
opts.format = formatOpt
opts.validateFormats = formatOpt
}

// Validate data using schema
// AnySchema will be compiled and cached using as a key JSON serialized with
// [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify)
validate<T = any>(schema: Schema | JSONSchemaType<T> | string, data: unknown): data is T
validate<T = any>(schema: AsyncSchema, data: unknown): Promise<T>
validate<T = any>(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise<T>
validate<T = any>(
validate(schema: Schema | string, data: unknown): boolean
validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise<unknown>
validate<T>(schema: Schema | JSONSchemaType<T> | string, data: unknown): data is T
validate<T>(schema: AsyncSchema, data: unknown | T): Promise<T>
validate<T>(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise<T>
validate<T>(
schemaKeyRef: AnySchema | string, // key, ref or schema object
data: unknown // to be validated
data: unknown | T // to be validated
): boolean | Promise<T> {
let v: AnyValidateFunction | undefined
if (typeof schemaKeyRef == "string") {
Expand All @@ -164,10 +193,10 @@ export default class Ajv {

// Create validation function for passed schema
// _meta: true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords.
compile<T = any>(schema: Schema | JSONSchemaType<T>, _meta?: boolean): ValidateFunction<T>
compile<T = any>(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction<T>
compile<T = any>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T>
compile<T = any>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T> {
compile<T = unknown>(schema: Schema | JSONSchemaType<T>, _meta?: boolean): ValidateFunction<T>
compile<T = unknown>(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction<T>
compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T>
compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T> {
const sch = this._addSchema(schema, _meta)
return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction<T>
}
Expand All @@ -176,14 +205,20 @@ export default class Ajv {
// `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
// TODO allow passing schema URI
// meta - optional true to compile meta-schema
compileAsync<T = any>(
compileAsync<T = unknown>(
schema: SchemaObject | JSONSchemaType<T>,
_meta?: boolean
): Promise<ValidateFunction<T>>
compileAsync<T = any>(schema: AsyncSchema, meta?: boolean): Promise<AsyncValidateFunction<T>>
compileAsync<T = unknown>(schema: AsyncSchema, meta?: boolean): Promise<AsyncValidateFunction<T>>
// eslint-disable-next-line @typescript-eslint/unified-signatures
compileAsync<T = any>(schema: AnySchemaObject, meta?: boolean): Promise<AnyValidateFunction<T>>
compileAsync<T = any>(schema: AnySchemaObject, meta?: boolean): Promise<AnyValidateFunction<T>> {
compileAsync<T = unknown>(
schema: AnySchemaObject,
meta?: boolean
): Promise<AnyValidateFunction<T>>
compileAsync<T = unknown>(
schema: AnySchemaObject,
meta?: boolean
): Promise<AnyValidateFunction<T>> {
if (typeof this.opts.loadSchema != "function") {
throw new Error("options.loadSchema should be a function")
}
Expand Down Expand Up @@ -298,7 +333,7 @@ export default class Ajv {

// Get compiled schema by `key` or `ref`.
// (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id)
getSchema<T = any>(keyRef: string): AnyValidateFunction<T> | undefined {
getSchema<T = unknown>(keyRef: string): AnyValidateFunction<T> | undefined {
let sch
while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch
if (sch === undefined) {
Expand Down Expand Up @@ -510,12 +545,17 @@ export interface ErrorsTextOptions {
dataVar?: string
}

function checkDeprecatedOptions(this: Ajv, opts: Options): void {
if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath")
if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId")
if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems")
if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax")
if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode")
function checkOptions(
this: Ajv,
checkOpts: OptionsInfo<RemovedOptions | DeprecatedOptions>,
options: Options & RemovedOptions,
msg: string,
log: "warn" | "error" = "error"
): void {
for (const key in checkOpts) {
const opt = key as keyof typeof checkOpts
if (opt in options) this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`)
}
}

function defaultMeta(this: Ajv): string | AnySchemaObject | undefined {
Expand Down
29 changes: 19 additions & 10 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ export type LoadSchemaFunction = (
cb?: (err: Error | null, schema?: AnySchemaObject) => void
) => Promise<AnySchemaObject>

export type Options = CurrentOptions & DeprecatedOptions

export interface CurrentOptions {
strict?: boolean | "log"
$data?: boolean
allErrors?: boolean
verbose?: boolean
format?: false
formats?: {[name: string]: Format}
keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated
unknownFormats?: true | string[] | "ignore"
schemas?: AnySchema[] | {[key: string]: AnySchema}
missingRefs?: true | "ignore" | "fail"
extendRefs?: true | "ignore" | "fail"
Expand Down Expand Up @@ -73,32 +73,40 @@ export interface CurrentOptions {
| true
| ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)
allowMatchingProperties?: boolean // disables a strict mode restriction
validateFormats?: boolean
}

export interface CodeOptions {
formats?: Code // code to require (or construct) map of available formats - for standalone code
}

export interface Options extends CurrentOptions {
// removed:
export interface DeprecatedOptions {
jsPropertySyntax?: boolean // added instead of jsonPointers
unicode?: boolean
}

export interface RemovedOptions {
format?: boolean
errorDataPath?: "object" | "property"
nullable?: boolean // "nullable" keyword is supported by default
jsonPointers?: boolean
schemaId?: string
strictDefaults?: boolean
strictKeywords?: boolean
strictNumbers?: boolean
uniqueItems?: boolean
// deprecated:
jsPropertySyntax?: boolean // added instead of jsonPointers
unicode?: boolean
unknownFormats?: true | string[] | "ignore"
}

export interface InstanceOptions extends Options {
[opt: string]: unknown
strict: boolean | "log"
code: CodeOptions
loopRequired: number
loopEnum: number
serialize: (schema: AnySchema) => unknown
addUsedSchema: boolean
validateSchema: boolean | "log"
validateFormats: boolean
}

export interface Logger {
Expand All @@ -119,7 +127,7 @@ interface SourceCode {
scope: Scope
}

export interface ValidateFunction<T = any> {
export interface ValidateFunction<T = unknown> {
(
this: Ajv | any,
data: any,
Expand All @@ -134,7 +142,7 @@ export interface ValidateFunction<T = any> {
source?: SourceCode
}

export interface AsyncValidateFunction<T = any> extends ValidateFunction<T> {
export interface AsyncValidateFunction<T = unknown> extends ValidateFunction<T> {
(...args: Parameters<ValidateFunction<T>>): Promise<T>
$async: true
}
Expand Down Expand Up @@ -302,6 +310,7 @@ export interface AsyncFormatDefinition<T extends string | number> {
}

export type AddedFormat =
| true
| RegExp
| FormatValidator<string>
| FormatDefinition<string>
Expand Down
20 changes: 9 additions & 11 deletions lib/vocabularies/format/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
} from "../../types"
import type KeywordCxt from "../../compile/context"
import {_, str, nil, or, Code, getProperty} from "../../compile/codegen"
import N from "../../compile/names"

type FormatValidate =
| FormatValidator<string>
Expand All @@ -17,6 +16,7 @@ type FormatValidate =
| AsyncFormatValidator<number>
| RegExp
| string
| true

export type FormatError = ErrorObject<"format", {format: string}>

Expand All @@ -34,7 +34,7 @@ const def: CodeKeywordDefinition = {
code(cxt: KeywordCxt, ruleType?: string) {
const {gen, data, $data, schema, schemaCode, it} = cxt
const {opts, errSchemaPath, schemaEnv, self} = it
if (opts.format === false) return
if (!opts.validateFormats) return

if ($data) validate$DataFormat()
else validateFormat()
Expand All @@ -56,20 +56,18 @@ const def: CodeKeywordDefinition = {
cxt.fail$data(or(unknownFmt(), invalidFmt())) // TODO this is not tested. Possibly require ajv-formats to test formats in ajv as well

function unknownFmt(): Code {
if (opts.unknownFormats === "ignore") return nil
let unknown = _`${schemaCode} && !${format}`
if (Array.isArray(opts.unknownFormats)) {
unknown = _`${unknown} && !${N.self}.opts.unknownFormats.includes(${schemaCode})`
}
return _`(${unknown})`
if (opts.strict === false) return nil
return _`(${schemaCode} && !${format})`
}

function invalidFmt(): Code {
const callFormat = schemaEnv.$async
? _`${fDef}.async ? await ${format}(${data}) : ${format}(${data})`
: _`${format}(${data})`
const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})`
return _`(${format} && ${fType} === ${ruleType as string} && !(${validData}))`
return _`(${format} && ${format} !== true && ${fType} === ${
ruleType as string
} && !(${validData}))`
}
}

Expand All @@ -79,15 +77,15 @@ const def: CodeKeywordDefinition = {
unknownFormat()
return
}
if (formatDef === true) return
const [fmtType, format, fmtRef] = getFormat(formatDef)
if (fmtType === ruleType) cxt.pass(validCondition())

function unknownFormat(): void {
if (opts.unknownFormats === "ignore") {
if (opts.strict === false) {
self.logger.warn(unknownMsg())
return
}
if (Array.isArray(opts.unknownFormats) && opts.unknownFormats.includes(schema)) return
throw new Error(unknownMsg())

function unknownMsg(): string {
Expand Down
2 changes: 1 addition & 1 deletion spec/extras.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {afterError, afterEach} from "./after_test"

const instances = getAjvInstances(options, {
$data: true,
unknownFormats: ["allowedUnknown"],
formats: {allowedUnknown: true},
})

jsonSchemaTest(instances, {
Expand Down
7 changes: 6 additions & 1 deletion spec/json-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_j
runTest(
getAjvInstances(options, {
strict: false,
unknownFormats: ["idn-email", "idn-hostname", "iri", "iri-reference"],
formats: {
"idn-email": true,
"idn-hostname": true,
iri: true,
"iri-reference": true,
},
}),
7,
require("./_json/draft7")
Expand Down
2 changes: 1 addition & 1 deletion spec/options/options_validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("validation options", () => {
describe("format", () => {
it("should not validate formats if option format == false", () => {
const ajv = new _Ajv({formats: {date: DATE_FORMAT}}),
ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, format: false})
ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, validateFormats: false})

const schema = {format: "date"}
const invalideDateTime = "06/19/1963" // expects hyphens
Expand Down
2 changes: 0 additions & 2 deletions spec/options/schemaId.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const should = require("../chai").should()
describe("removed schemaId option", () => {
it("should use $id and throw exception when id is used", () => {
test(new _Ajv({logger: false}))
test(new _Ajv({schemaId: "$id", logger: false}))

function test(ajv) {
ajv.addSchema({$id: "mySchema1", type: "string"})
Expand All @@ -18,7 +17,6 @@ describe("removed schemaId option", () => {

it("should use $id and ignore id when strict: false", () => {
test(new _Ajv({logger: false, strict: false}))
test(new _Ajv({schemaId: "$id", logger: false, strict: false}))

function test(ajv) {
ajv.addSchema({$id: "mySchema1", type: "string"})
Expand Down
Loading

0 comments on commit 080ee7d

Please sign in to comment.