Skip to content

Commit

Permalink
feat: implement defaultUnevaluatedProperties instead of `defaultAdd…
Browse files Browse the repository at this point in the history
…itionalProperties` (#12)
  • Loading branch information
RomanHotsiy authored Sep 19, 2022
1 parent 0659d0d commit 15b7e13
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 15 deletions.
1 change: 1 addition & 0 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface SchemaCxt {
readonly createErrors?: boolean
readonly opts: InstanceOptions // Ajv instance option.
readonly self: Ajv // current Ajv instance
readonly isAllOfVariant?: boolean // if the current schema is direct child of allOf
}

export interface SchemaObjCxt extends SchemaCxt {
Expand Down
1 change: 1 addition & 0 deletions lib/compile/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const names = {
parentDataProperty: new Name("parentDataProperty"),
rootData: new Name("rootData"), // root data - same as the data passed to the first/top validation function
dynamicAnchors: new Name("dynamicAnchors"), // used to support recursiveRef and dynamicRef
isAllOfVariant: new Name("isAllOfVariant"), // used to check in runtime if the current function (ref) is called from allOf
// function scoped variables
vErrors: new Name("vErrors"), // null or array of validation errors
errors: new Name("errors"), // counter of validation errors
Expand Down
25 changes: 21 additions & 4 deletions lib/compile/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function validateFunction(
function destructureValCxt(opts: InstanceOptions): Code {
return _`{${N.instancePath}="", ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}=${
N.data
}${opts.dynamicRef ? _`, ${N.dynamicAnchors}={}` : nil}}={}`
}${opts.dynamicRef ? _`, ${N.dynamicAnchors}={}` : nil}, ${N.isAllOfVariant} = 0}={}`
}

function destructureValCxtES5(gen: CodeGen, opts: InstanceOptions): void {
Expand All @@ -77,13 +77,15 @@ function destructureValCxtES5(gen: CodeGen, opts: InstanceOptions): void {
gen.var(N.parentData, _`${N.valCxt}.${N.parentData}`)
gen.var(N.parentDataProperty, _`${N.valCxt}.${N.parentDataProperty}`)
gen.var(N.rootData, _`${N.valCxt}.${N.rootData}`)
gen.var(N.isAllOfVariant, _`${N.valCxt}.${N.isAllOfVariant}`)
if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`${N.valCxt}.${N.dynamicAnchors}`)
},
() => {
gen.var(N.instancePath, _`""`)
gen.var(N.parentData, _`undefined`)
gen.var(N.parentDataProperty, _`undefined`)
gen.var(N.rootData, N.data)
gen.var(N.isAllOfVariant, _`0`)
if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`{}`)
}
)
Expand Down Expand Up @@ -261,11 +263,20 @@ function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void {
if (useDefaults) assignDefaults(it, group.type)
gen.block(() => {
for (const rule of group.rules) {
if (shouldUseRule(schema, rule)) {
if (shouldUseRule(schema, rule) || shouldForceUnevaluatedProperties(schema, rule)) {
keywordCode(it, rule.keyword, rule.definition, group.type)
}
}
})

function shouldForceUnevaluatedProperties(schema: AnySchemaObject, rule: Rule): boolean {
return !!(
rule.keyword === "unevaluatedProperties" &&
(schema.properties || schema.patternProperties) &&
!it.isAllOfVariant &&
it.opts.defaultUnevaluatedProperties === false
)
}
}

function checkStrictTypes(it: SchemaObjCxt, types: JSONType[]): void {
Expand Down Expand Up @@ -483,11 +494,17 @@ export class KeywordCxt implements KeywordErrorCxt {
}
}

subschema(appl: SubschemaArgs, valid: Name): SchemaCxt {
subschema(appl: SubschemaArgs, valid: Name, isAllOfVariant?: boolean): SchemaCxt {
const subschema = getSubschema(this.it, appl)
extendSubschemaData(subschema, this.it, appl)
extendSubschemaMode(subschema, appl)
const nextContext = {...this.it, ...subschema, items: undefined, props: undefined}
const nextContext = {
...this.it,
...subschema,
items: undefined,
props: undefined,
isAllOfVariant,
}
subschemaCode(nextContext, valid)
return nextContext
}
Expand Down
11 changes: 9 additions & 2 deletions lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ import DefaultUriResolver from "./runtime/uri"
const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
defaultRegExp.code = "new RegExp"

const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"]
const META_IGNORE_OPTIONS: (keyof Options)[] = [
"removeAdditional",
"useDefaults",
"coerceTypes",
"defaultUnevaluatedProperties",
"defaultAdditionalProperties",
]
const EXT_SCOPE_NAMES = new Set([
"validate",
"serialize",
Expand Down Expand Up @@ -118,7 +124,8 @@ export interface CurrentOptions {
loadSchemaSync?: (base: string, $ref: string, id: string) => AnySchemaObject | boolean
// options to modify validated data:
removeAdditional?: boolean | "all" | "failing"
defaultAdditionalProperties?: boolean
defaultUnevaluatedProperties?: boolean
defaultAdditionalProperties?: boolean // @deprecated

useDefaults?: boolean | "empty"
coerceTypes?: boolean | "array"
Expand Down
2 changes: 1 addition & 1 deletion lib/vocabularies/applicator/allOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const def: CodeKeywordDefinition = {
const valid = gen.name("valid")
schema.forEach((sch: AnySchema, i: number) => {
if (alwaysValidSchema(it, sch)) return
const schCxt = cxt.subschema({keyword: "allOf", schemaProp: i}, valid)
const schCxt = cxt.subschema({keyword: "allOf", schemaProp: i}, valid, true)
cxt.ok(valid)
cxt.mergeEvaluated(schCxt)
})
Expand Down
1 change: 1 addition & 0 deletions lib/vocabularies/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function callValidateCode(
[N.parentData, it.parentData],
[N.parentDataProperty, it.parentDataProperty],
[N.rootData, N.rootData],
[N.isAllOfVariant, it.isAllOfVariant ? 1 : 0],
]
if (it.opts.dynamicRef) valCxt.push([N.dynamicAnchors, N.dynamicAnchors])
const args = _`${dataAndSchema}, ${gen.object(...valCxt)}`
Expand Down
3 changes: 2 additions & 1 deletion lib/vocabularies/core/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const def: CodeKeywordDefinition = {
topSchemaRef: schName,
errSchemaPath: $ref,
},
valid
valid,
it.isAllOfVariant // pass isAllOfVariant into inlined $ref
)
cxt.mergeEvaluated(schCxt)
cxt.ok(valid)
Expand Down
34 changes: 27 additions & 7 deletions lib/vocabularies/unevaluated/unevaluatedProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ const def: CodeKeywordDefinition = {
keyword: "unevaluatedProperties",
type: "object",
schemaType: ["boolean", "object"],
allowUndefined: true,
trackErrors: true,
error,
code(cxt) {
const {gen, schema, data, errsCount, it} = cxt
const {gen, schema = cxt.it.opts.defaultUnevaluatedProperties, data, errsCount, it} = cxt
const isForced = cxt.schema === undefined && cxt.it.opts.defaultUnevaluatedProperties === false
/* istanbul ignore if */
if (!errsCount) throw new Error("ajv implementation error")
const {allErrors, props} = it
Expand All @@ -37,13 +39,31 @@ const def: CodeKeywordDefinition = {
)
)
} else if (props !== true) {
gen.forIn("key", data, (key: Name) =>
props === undefined
? unevaluatedPropCode(key)
: gen.if(unevaluatedStatic(props, key), () => unevaluatedPropCode(key))
)
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const staticCheck = () =>
gen.forIn("key", data, (key: Name) =>
props === undefined
? unevaluatedPropCode(key)
: gen.if(unevaluatedStatic(props, key), () => unevaluatedPropCode(key))
)

if (isForced && it.errorPath.emptyStr()) {
// $refs are compiled into functions
// We need to check in runtime if function was called from allOf.
// We need to check only on the top level of the function:
// it is ensured with `it.errorPath.emptyStr()` check
gen.if(_`${N.isAllOfVariant} === 0`, staticCheck)
} else {
staticCheck()
}
}

if (!isForced) {
// disable shot-circut for forced unevaluatedProperties=false
// we may run or not the check in runtime so we can't short-circuit in compile-time
it.props = true
}
it.props = true

cxt.ok(_`${errsCount} === ${N.errors}`)

function unevaluatedPropCode(key: Name): void {
Expand Down
Loading

0 comments on commit 15b7e13

Please sign in to comment.