Skip to content

Commit

Permalink
Merge 364dfde into 45583fd
Browse files Browse the repository at this point in the history
  • Loading branch information
pkuczynski committed Apr 6, 2023
2 parents 45583fd + 364dfde commit eac6651
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 29 deletions.
30 changes: 19 additions & 11 deletions lib/vocabularies/discriminator/index.ts
@@ -1,6 +1,6 @@
import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Name} from "../../compile/codegen"
import {_, or, getProperty, Name} from "../../compile/codegen"
import {DiscrError, DiscrErrorObj} from "../discriminator/types"
import {resolveRef, SchemaEnv} from "../../compile"
import {schemaHasRulesButRef} from "../../compile/util"
Expand Down Expand Up @@ -33,8 +33,9 @@ const def: CodeKeywordDefinition = {
if (!oneOf) throw new Error("discriminator: requires oneOf keyword")
const valid = gen.let("valid", false)
const tag = gen.const("tag", _`${data}${getProperty(tagName)}`)
const tagTypes = ["string", "number", "boolean"]
gen.if(
_`typeof ${tag} == "string"`,
or(...tagTypes.map((t) => _`typeof ${tag} == ${t}`), _`${tag} == null`),
() => validateMapping(),
() => cxt.error(false, {discrError: DiscrError.Tag, tag, tagName})
)
Expand All @@ -43,9 +44,9 @@ const def: CodeKeywordDefinition = {
function validateMapping(): void {
const mapping = getMapping()
gen.if(false)
for (const tagValue in mapping) {
for (const tagValue of mapping.keys()) {
gen.elseIf(_`${tag} === ${tagValue}`)
gen.assign(valid, applyTagSchema(mapping[tagValue]))
gen.assign(valid, applyTagSchema(mapping.get(tagValue)))
}
gen.else()
cxt.error(false, {discrError: DiscrError.Mapping, tag, tagName})
Expand All @@ -59,8 +60,10 @@ const def: CodeKeywordDefinition = {
return _valid
}

function getMapping(): {[T in string]?: number} {
const oneOfMapping: {[T in string]?: number} = {}
type OneOfMapping = Map<string | number | boolean | null, number>

function getMapping(): OneOfMapping {
const oneOfMapping: OneOfMapping = new Map()
const topRequired = hasRequired(parentSchema)
let tagRequired = true
for (let i = 0; i < oneOf.length; i++) {
Expand All @@ -86,7 +89,7 @@ const def: CodeKeywordDefinition = {
}

function addMappings(sch: AnySchemaObject, i: number): void {
if (sch.const) {
if (sch.const !== undefined) {
addMapping(sch.const, i)
} else if (sch.enum) {
for (const tagValue of sch.enum) {
Expand All @@ -97,11 +100,16 @@ const def: CodeKeywordDefinition = {
}
}

function addMapping(tagValue: unknown, i: number): void {
if (typeof tagValue != "string" || tagValue in oneOfMapping) {
throw new Error(`discriminator: "${tagName}" values must be unique strings`)
function addMapping(tagValue: any, i: number): void {
if (
!(["string", "number", "boolean"].includes(typeof tagValue) || tagValue === null) ||
oneOfMapping.has(tagValue)
) {
throw new Error(
`discriminator: "${tagName}" values must be unique strings, numbers, booleans or nulls`
)
}
oneOfMapping[tagValue] = i
oneOfMapping.set(tagValue, i)
}
}
},
Expand Down
193 changes: 175 additions & 18 deletions spec/discriminator.spec.ts
Expand Up @@ -23,7 +23,7 @@ describe("discriminator keyword", function () {
}

describe("validation", () => {
const schema1 = {
const stringSchema1 = {
type: "object",
discriminator: {propertyName: "foo"},
oneOf: [
Expand All @@ -44,7 +44,7 @@ describe("discriminator keyword", function () {
],
}

const schema2 = {
const stringSchema2 = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
Expand All @@ -66,18 +66,151 @@ describe("discriminator keyword", function () {
],
}

const schemas = [schema1, schema2]
const numberSchema = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
properties: {
foo: {const: 1},
a: {type: "string"},
},
required: ["a"],
},
{
properties: {
foo: {enum: [2, 3]},
b: {type: "string"},
},
required: ["b"],
},
],
}

it("should validate data", () => {
assertValid(schemas, {foo: "x", a: "a"})
assertValid(schemas, {foo: "y", b: "b"})
assertValid(schemas, {foo: "z", b: "b"})
assertInvalid(schemas, {})
assertInvalid(schemas, {foo: 1})
assertInvalid(schemas, {foo: "bar"})
assertInvalid(schemas, {foo: "x", b: "b"})
assertInvalid(schemas, {foo: "y", a: "a"})
assertInvalid(schemas, {foo: "z", a: "a"})
const boolSchema = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
properties: {
foo: {const: true},
a: {type: "string"},
},
required: ["a"],
},
{
properties: {
foo: {const: false},
b: {type: "string"},
},
required: ["b"],
},
],
}

const mixedSchema = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
properties: {
foo: {const: "x"},
a: {type: "string"},
},
required: ["a"],
},
{
properties: {
foo: {enum: [1, 2]},
b: {type: "string"},
},
required: ["b"],
},
{
properties: {
foo: {const: true},
c: {type: "string"},
},
required: ["c"],
},
{
properties: {
foo: {const: null},
d: {type: "number"},
},
required: ["d"],
},
],
}

const stringSchemas = [stringSchema1, stringSchema2]
const numberSchemas = [numberSchema]
const boolSchemas = [boolSchema]
const mixedSchemas = [mixedSchema]

it("should validate data for string discriminator", () => {
assertValid(stringSchemas, {foo: "x", a: "a"})
assertValid(stringSchemas, {foo: "y", b: "b"})
assertValid(stringSchemas, {foo: "z", b: "b"})

assertInvalid(stringSchemas, {})
assertInvalid(stringSchemas, {foo: 1})
assertInvalid(stringSchemas, {foo: "bar"})
assertInvalid(stringSchemas, {foo: "x", b: "b"})
assertInvalid(stringSchemas, {foo: "y", a: "a"})
assertInvalid(stringSchemas, {foo: "z", a: "a"})
})

it("should validate data for number discriminator", () => {
assertValid(numberSchemas, {foo: 1, a: "a"})
assertValid(numberSchemas, {foo: 2, b: "b"})
assertValid(numberSchemas, {foo: 3, b: "b"})

assertInvalid(stringSchemas, {})
assertInvalid(stringSchemas, {foo: "1"})
assertInvalid(stringSchemas, {foo: "bar"})
assertInvalid(numberSchemas, {foo: 1, b: "b"})
assertInvalid(numberSchemas, {foo: 2, a: "a"})
assertInvalid(numberSchemas, {foo: 3, a: "a"})
})

it("should validate data for boolean discriminator", () => {
assertValid(boolSchemas, {foo: true, a: "a"})
assertValid(boolSchemas, {foo: false, b: "b"})

assertInvalid(boolSchemas, {})
assertInvalid(boolSchemas, {foo: "1"})
assertInvalid(boolSchemas, {foo: true, b: "b"})
assertInvalid(boolSchemas, {foo: false, a: "a"})
})

it("should validate data for mixed type discriminator", () => {
assertValid(mixedSchemas, {foo: "x", a: "a"})
assertValid(mixedSchemas, {foo: 1, b: "b"})
assertValid(mixedSchemas, {foo: 2, b: "b"})
assertValid(mixedSchemas, {foo: true, c: "c"})
assertValid(mixedSchemas, {foo: null, d: 123})

assertInvalid(mixedSchemas, {})
assertInvalid(mixedSchemas, {foo: "x"})
assertInvalid(mixedSchemas, {foo: "x", b: "b"})
assertInvalid(mixedSchemas, {foo: "x", c: "c"})
assertInvalid(mixedSchemas, {foo: 1})
assertInvalid(mixedSchemas, {foo: 1, a: "a"})
assertInvalid(mixedSchemas, {foo: 1, c: "c"})
assertInvalid(mixedSchemas, {foo: 2})
assertInvalid(mixedSchemas, {foo: 2, a: "a"})
assertInvalid(mixedSchemas, {foo: 2, c: "c"})
assertInvalid(mixedSchemas, {foo: true})
assertInvalid(mixedSchemas, {foo: true, a: "a"})
assertInvalid(mixedSchemas, {foo: true, b: "b"})
assertInvalid(mixedSchemas, {foo: null})
assertInvalid(mixedSchemas, {foo: null, a: "a"})
assertInvalid(mixedSchemas, {foo: null, b: "b"})
assertInvalid(mixedSchemas, {foo: null, c: "c"})
})
})

Expand Down Expand Up @@ -273,15 +406,15 @@ describe("discriminator keyword", function () {
)
})

it("tag value should be string", () => {
it("tag value should be string, number, boolean or null", () => {
invalidSchema(
{
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [{properties: {foo: {const: 1}}}],
oneOf: [{properties: {foo: {const: {baz: "bar"}}}}],
},
/discriminator: "foo" values must be unique strings/
/discriminator: "foo" values must be unique strings, numbers, booleans or nulls/
)
})

Expand All @@ -293,7 +426,27 @@ describe("discriminator keyword", function () {
required: ["foo"],
oneOf: [{properties: {foo: {const: "a"}}}, {properties: {foo: {const: "a"}}}],
},
/discriminator: "foo" values must be unique strings/
/discriminator: "foo" values must be unique strings, numbers, booleans or nulls/
)

invalidSchema(
{
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [{properties: {foo: {const: 1}}}, {properties: {foo: {const: 1}}}],
},
/discriminator: "foo" values must be unique/
)

invalidSchema(
{
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [{properties: {foo: {const: true}}}, {properties: {foo: {const: true}}}],
},
/discriminator: "foo" values must be unique/
)
})

Expand All @@ -314,7 +467,11 @@ describe("discriminator keyword", function () {

function assertValid(schemas: SchemaObject[], data: unknown): void {
schemas.forEach((schema) =>
ajvs.forEach((ajv) => assert.strictEqual(ajv.validate(schema, data), true))
ajvs.forEach((ajv) => {
const validate = ajv.compile(schema)
const valid = validate(data)
assert.strictEqual(valid, true)
})
)
}

Expand Down

0 comments on commit eac6651

Please sign in to comment.