Skip to content

Commit

Permalink
OpenAPI discriminator, tests, #1119
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Mar 16, 2021
1 parent f04ad11 commit 3253edd
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 164 deletions.
143 changes: 42 additions & 101 deletions lib/vocabularies/discriminator/index.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,52 @@
import type {CodeKeywordDefinition, SchemaObject} from "../../types"
import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Name} from "../../compile/codegen"
import {DiscrError, DiscrErrorObj} from "../discriminator/types"

interface DiscriminatorSchema {
propertyName: string
mapping?: Record<string, string | undefined>
}
export type DiscriminatorError = DiscrErrorObj<DiscrError.Tag> | DiscrErrorObj<DiscrError.Mapping>

const metaSchema = {
type: "object",
properties: {
propertyName: {type: "string"},
mapping: {
type: "object",
additionalProperties: {type: "string"},
},
},
required: ["propertyName"],
additionalProperties: false,
const error: KeywordErrorDefinition = {
message: ({params: {discrError, tagName}}) =>
discrError === DiscrError.Tag
? `tag "${tagName}" must be string`
: `value of tag "${tagName}" must be in oneOf`,
params: ({params: {discrError, tag, tagName}}) =>
_`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}`,
}

const def: CodeKeywordDefinition = {
keyword: "discriminator",
type: "object",
schemaType: "object",
metaSchema,
error,
code(cxt: KeywordCxt) {
const {gen, data, schema, parentSchema, it} = cxt
const {oneOf} = parentSchema
if (!it.opts.discriminator) {
throw new Error("discriminator: requires discriminator option")
}
if (!oneOf) throw new Error("discriminator: requires oneOf")
const tagName = schema.propertyName
if (typeof tagName != "string") throw new Error("discriminator: requires propertyName")
if (schema.mapping) throw new Error("discriminator: mapping is not supported")
if (!oneOf) throw new Error("discriminator: requires oneOf keyword")
const valid = gen.let("valid", false)
const tag = gen.const("tag", _`${data}${getProperty(schema.propertyName)}`)
cxt.setParams({discrError: "tag", tag})
const oneOfIndex = getOneOfMapping(schema)
if (schema.mapping) {
validateMapping()
} else {
validateDiscriminator()
}
const tag = gen.const("tag", _`${data}${getProperty(tagName)}`)
gen.if(
_`typeof ${tag} == "string"`,
() => validateMapping(),
() => cxt.error(false, {discrError: DiscrError.Tag, tag, tagName})
)
cxt.ok(valid)

function validateDiscriminator(): void {
gen.if(
_`typeof ${tag} == "string"`,
() => validateMapping(),
() => cxt.error()
)
}

function validateMapping(): void {
const mapping = getMapping()
gen.if(false)
for (const tagValue in oneOfIndex) {
for (const tagValue in mapping) {
gen.elseIf(_`${tag} === ${tagValue}`)
gen.assign(valid, applyTagSchema(oneOfIndex[tagValue]))
}
if (!schema.mapping) {
gen.else()
cxt.error()
gen.assign(valid, applyTagSchema(mapping[tagValue]))
}
gen.else()
cxt.error(false, {discrError: DiscrError.Mapping, tag, tagName})
gen.endIf()
}

Expand All @@ -71,50 +57,27 @@ const def: CodeKeywordDefinition = {
return _valid
}

function getOneOfMapping({
propertyName: tagName,
mapping,
}: DiscriminatorSchema): {[T in string]?: number} {
function getMapping(): {[T in string]?: number} {
const oneOfMapping: {[T in string]?: number} = {}
const tagRequired = isRequired(parentSchema.required)
if (mapping) {
const refs: {[T in string]?: number} = {}
for (let i = 0; i < oneOf.length; i++) {
const ref = oneOf[i].$ref
if (typeof ref != "string") {
throw new Error(`discriminator: oneOf schemas must have "$ref"`)
}
refs[ref] = i
}
for (const tagValue in mapping) {
checkUniqueTagValue(tagValue)
const ref = mapping[tagValue] as string
if (refs[ref] === undefined) {
throw new Error(`discriminator: mapping has ref not in oneOf`)
}
oneOfMapping[tagValue] = refs[ref]
const topRequired = hasRequired(parentSchema)
let tagRequired = true
for (let i = 0; i < oneOf.length; i++) {
const sch = oneOf[i]
const propSch = sch.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`)
}
checkTagProperty()
} else {
let innerTagRequired = true
for (let i = 0; i < oneOf.length; i++) {
const sch = oneOf[i]
const propSch = sch.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`)
}
innerTagRequired = innerTagRequired && (tagRequired || isRequired(sch.required))
addMappings(propSch, i)
}
if (!innerTagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
tagRequired = tagRequired && (topRequired || hasRequired(sch))
addMappings(propSch, i)
}
if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
return oneOfMapping

function isRequired(req: unknown): boolean {
return Array.isArray(req) && req.includes(tagName)
function hasRequired({required}: AnySchemaObject): boolean {
return Array.isArray(required) && required.includes(tagName)
}

function addMappings(sch: SchemaObject, i: number): void {
function addMappings(sch: AnySchemaObject, i: number): void {
if (sch.const) {
addMapping(sch.const, i)
} else if (sch.enum) {
Expand All @@ -127,33 +90,11 @@ const def: CodeKeywordDefinition = {
}

function addMapping(tagValue: unknown, i: number): void {
if (typeof tagValue != "string") {
throw new Error(`discriminator: "${tagName}" property must be string`)
if (typeof tagValue != "string" || tagValue in oneOfMapping) {
throw new Error(`discriminator: "${tagName}" values must be unique strings`)
}
checkUniqueTagValue(tagValue)
oneOfMapping[tagValue] = i
}

function checkUniqueTagValue(tagValue: string): void {
if (tagValue in oneOfMapping) {
throw new Error(`discriminator: duplicate "${tagName}" property value "${tagValue}"`)
}
}

function checkTagProperty(): void {
const values = parentSchema.properties?.[tagName]?.enum
if (Array.isArray(values)) {
const tagValues = new Set(values)
const mappingValues = Object.keys(oneOfMapping)
if (
tagValues.size === mappingValues.length &&
mappingValues.every((v) => tagValues.has(v))
) {
return
}
}
throw new Error(`discriminator: "properties/${tagName}/enum" must match mapping`)
}
}
},
}
Expand Down
12 changes: 12 additions & 0 deletions lib/vocabularies/discriminator/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {ErrorObject} from "../../types"

export enum DiscrError {
Tag = "tag",
Mapping = "mapping",
}

export type DiscrErrorObj<E extends DiscrError> = ErrorObject<
"discriminator",
{error: E; tag: string; tagValue: unknown},
string
>
2 changes: 2 additions & 0 deletions lib/vocabularies/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {FormatError} from "./format/format"
import type {UnevaluatedPropertiesError} from "./unevaluated/unevaluatedProperties"
import type {UnevaluatedItemsError} from "./unevaluated/unevaluatedItems"
import type {DependentRequiredError} from "./validation/dependentRequired"
import type {DiscriminatorError} from "./discriminator"

export type DefinedError =
| TypeError
Expand All @@ -14,3 +15,4 @@ export type DefinedError =
| UnevaluatedPropertiesError
| UnevaluatedItemsError
| DependentRequiredError
| DiscriminatorError
14 changes: 2 additions & 12 deletions lib/vocabularies/jtd/discriminator.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import type {CodeKeywordDefinition, KeywordErrorDefinition, ErrorObject} from "../../types"
import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, not, getProperty, Name} from "../../compile/codegen"
import {checkMetadata} from "./metadata"
import {checkNullableObject} from "./nullable"
import {typeErrorMessage, typeErrorParams, _JTDTypeError} from "./error"

enum DiscrError {
Tag = "tag",
Mapping = "mapping",
}

type DiscrErrorObj<E extends DiscrError> = ErrorObject<
"discriminator",
{error: E; tag: string; tagValue: unknown},
string
>
import {DiscrError, DiscrErrorObj} from "../discriminator/types"

export type JTDDiscriminatorError =
| _JTDTypeError<"discriminator", "object", string>
Expand Down
7 changes: 6 additions & 1 deletion spec/ajv_standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type {Options} from ".."
import AjvPack from "../dist/standalone/instance"

export function withStandalone(instances: AjvCore[]): (AjvCore | AjvPack)[] {
return [...(instances as (AjvCore | AjvPack)[]), ...instances.map((ajv) => new AjvPack(ajv))]
return [...(instances as (AjvCore | AjvPack)[]), ...instances.map(makeStandalone)]
}

function makeStandalone(ajv: AjvCore): AjvPack {
ajv.opts.code.source = true
return new AjvPack(ajv)
}

export function getStandalone(_Ajv: typeof AjvCore, opts: Options = {}): AjvPack {
Expand Down
Loading

0 comments on commit 3253edd

Please sign in to comment.