Skip to content

Commit

Permalink
Merge bf63813 into 04775b1
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Sep 18, 2020
2 parents 04775b1 + bf63813 commit 28346ac
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 234 deletions.
548 changes: 357 additions & 191 deletions README.md

Large diffs are not rendered by default.

40 changes: 25 additions & 15 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 Down Expand Up @@ -65,7 +66,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 = ["removeAdditional", "useDefaults", "coerceTypes"] as const
const META_SUPPORT_DATA = ["/properties"]
const EXT_SCOPE_NAMES = new Set([
"validate",
Expand Down Expand Up @@ -141,12 +142,14 @@ export default class Ajv {
// 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 +167,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 +179,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 +307,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 @@ -514,6 +523,7 @@ 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.unknownFormats !== undefined) this.logger.error("NOT SUPPORTED: option unknownFormats")
if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax")
if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode")
}
Expand Down
10 changes: 5 additions & 5 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ export interface CurrentOptions {
$data?: boolean
allErrors?: boolean
verbose?: boolean
format?: false
format?: boolean
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 @@ -85,13 +84,13 @@ export interface Options extends CurrentOptions {
nullable?: boolean // "nullable" keyword is supported by default
schemaId?: string
uniqueItems?: boolean
unknownFormats?: true | string[] | "ignore"
// deprecated:
jsPropertySyntax?: boolean // added instead of jsonPointers
unicode?: boolean
}

export interface InstanceOptions extends Options {
[opt: string]: unknown
strict: boolean | "log"
code: CodeOptions
loopRequired: number
Expand Down Expand Up @@ -119,7 +118,7 @@ interface SourceCode {
scope: Scope
}

export interface ValidateFunction<T = any> {
export interface ValidateFunction<T = unknown> {
(
this: Ajv | any,
data: any,
Expand All @@ -134,7 +133,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 +301,7 @@ export interface AsyncFormatDefinition<T extends string | number> {
}

export type AddedFormat =
| true
| RegExp
| FormatValidator<string>
| FormatDefinition<string>
Expand Down
18 changes: 8 additions & 10 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 Down Expand Up @@ -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
12 changes: 5 additions & 7 deletions spec/options/unknownFormats.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import _Ajv from "../ajv"
const should = require("../chai").should()
const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/

describe("unknownFormats option", () => {
describe("specifying allowed unknown formats with `formats` option", () => {
describe("= true (default)", () => {
it("should fail schema compilation if unknown format is used", () => {
test(new _Ajv())
test(new _Ajv({unknownFormats: true}))

function test(ajv) {
should.throw(() => {
Expand All @@ -17,7 +16,6 @@ describe("unknownFormats option", () => {

it("should fail validation if unknown format is used via $data", () => {
test(new _Ajv({$data: true}))
test(new _Ajv({$data: true, unknownFormats: true}))

function test(ajv) {
ajv.addFormat("date", DATE_FORMAT)
Expand All @@ -40,7 +38,7 @@ describe("unknownFormats option", () => {

describe('= "ignore (default before 5.0.0)"', () => {
it("should pass schema compilation and be valid if unknown format is used", () => {
test(new _Ajv({unknownFormats: "ignore", logger: false}))
test(new _Ajv({strict: false, logger: false}))

function test(ajv) {
const validate = ajv.compile({format: "unknown"})
Expand All @@ -49,7 +47,7 @@ describe("unknownFormats option", () => {
})

it("should be valid if unknown format is used via $data", () => {
test(new _Ajv({$data: true, unknownFormats: "ignore"}))
test(new _Ajv({$data: true, strict: false}))

function test(ajv) {
ajv.addFormat("date", DATE_FORMAT)
Expand All @@ -71,7 +69,7 @@ describe("unknownFormats option", () => {

describe("= [String]", () => {
it("should pass schema compilation and be valid if allowed unknown format is used", () => {
test(new _Ajv({unknownFormats: ["allowed"]}))
test(new _Ajv({formats: {allowed: true}}))

function test(ajv) {
const validate = ajv.compile({format: "allowed"})
Expand All @@ -84,7 +82,7 @@ describe("unknownFormats option", () => {
})

it("should be valid if allowed unknown format is used via $data", () => {
test(new _Ajv({$data: true, unknownFormats: ["allowed"]}))
test(new _Ajv({$data: true, formats: {allowed: true}}))

function test(ajv) {
ajv.addFormat("date", DATE_FORMAT)
Expand Down
2 changes: 1 addition & 1 deletion spec/schema-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import options from "./ajv_options"
import {afterError, afterEach} from "./after_test"
import addFormats from "ajv-formats"

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

const remoteRefs = {
"http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"),
Expand Down
2 changes: 1 addition & 1 deletion spec/types/async-validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe("$async validation and type guards", () => {

it("should have result type boolean | promise 2", async () => {
const schema = {$async: false}
const validate = ajv.compile(schema)
const validate = ajv.compile<any>(schema)
const result = validate({})
if (typeof result === "boolean") {
should.exist(result)
Expand Down
4 changes: 2 additions & 2 deletions spec/types/json-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ describe("JSONSchemaType type and validation as a type guard", () => {

describe("schema has type JSONSchemaType<MyData>", () => {
it("should prove the type of validated data", () => {
const validate = ajv.compile<MyData>(mySchema)
const validate = ajv.compile(mySchema)
if (validate(validData)) {
validData.foo.should.equal("foo")
}
should.not.exist(validate.errors)

if (ajv.validate<MyData>(mySchema, validData)) {
if (ajv.validate(mySchema, validData)) {
validData.foo.should.equal("foo")
}
should.not.exist(ajv.errors)
Expand Down

0 comments on commit 28346ac

Please sign in to comment.