Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions lib/vocabularies/dynamic/dynamicRef.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {CodeKeywordDefinition} from "../../types"
import type {AnySchema, CodeKeywordDefinition} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Code, Name} from "../../compile/codegen"
import N from "../../compile/names"
import {callRef} from "../core/ref"
import {callRef, getValidate} from "../core/ref"
import {SchemaEnv, compileSchema} from "../../compile"

const def: CodeKeywordDefinition = {
keyword: "$dynamicRef",
Expand All @@ -29,14 +30,49 @@ export function dynamicRef(cxt: KeywordCxt, ref: string): void {
// Because of that 2 tests in recursiveRef.json fail.
// This is a similar problem to #815 (`$id` doesn't alter resolution scope for `{ "$ref": "#" }`).
// (This problem is not tested in JSON-Schema-Test-Suite)

// Step 1: Try to resolve anchor from localRefs if not already compiled
const anchorRef = `#${anchor}`
const anchorSchema = it.schemaEnv.root.localRefs?.[anchorRef]

// Step 2: If found in localRefs but not yet compiled, compile it
let anchorValidate: Code | undefined
if (anchorSchema && !it.schemaEnv.root.dynamicAnchors[anchor]) {
const sch = compileAnchorSchema(anchorSchema)
if (sch) {
anchorValidate = getValidate(cxt, sch)
// Mark as having dynamic anchor so we use dynamic lookup
it.schemaEnv.root.dynamicAnchors[anchor] = true
}
}

// Step 3: Generate dynamic lookup code
if (it.schemaEnv.root.dynamicAnchors[anchor]) {
// Pre-register if we just compiled the anchor schema
if (anchorValidate) {
const av = anchorValidate
gen.if(_`!${N.dynamicAnchors}${getProperty(anchor)}`, () =>
gen.assign(_`${N.dynamicAnchors}${getProperty(anchor)}`, av)
)
}
// Dynamic lookup at runtime
const v = gen.let("_v", _`${N.dynamicAnchors}${getProperty(anchor)}`)
gen.if(v, _callRef(v, valid), _callRef(it.validateName, valid))
} else {
_callRef(it.validateName, valid)()
}
}

function compileAnchorSchema(schema: AnySchema): SchemaEnv | undefined {
const {schemaEnv, self} = it
const {root, baseId, localRefs, meta} = schemaEnv.root
const {schemaId} = self.opts

const sch = new SchemaEnv({schema, schemaId, root, baseId, localRefs, meta})
compileSchema.call(self, sch)
return sch
}

function _callRef(validate: Code, valid?: Name): () => void {
return valid
? () =>
Expand Down
26 changes: 26 additions & 0 deletions spec/dynamic-ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ describe("recursiveRef and dynamicRef", () => {
})

describe("dynamicRef", () => {
it("should resolve $dynamicRef to $dynamicAnchor in $defs", () => {
// This test demonstrates the bug: $dynamicAnchor in $defs is not found by $dynamicRef
// because schemas in $defs are only compiled when referenced by $ref
const schema = {
type: "object",
properties: {
test: {$dynamicRef: "#meta"},
},
$defs: {
schema: {
$dynamicAnchor: "meta",
type: ["object", "boolean"],
},
},
}

ajvs.forEach((ajv) => {
const validate = ajv.compile(schema)
// $dynamicRef should resolve to the $defs/schema which allows object or boolean
assert.strictEqual(validate({test: true}), true, "boolean should be valid")
assert.strictEqual(validate({test: {}}), true, "object should be valid")
assert.strictEqual(validate({test: "string"}), false, "string should be invalid")
assert.strictEqual(validate({test: 123}), false, "number should be invalid")
})
})

it("should allow extending recursive schema with dynamicRef (future draft2020)", () => {
const treeSchema = {
$id: "https://example.com/tree",
Expand Down
Loading