in compiler/src/steps/validate-model.ts [52:959]
export default async function validateModel (apiModel: model.Model, restSpec: Map<string, JsonSpec>, errors: ValidationErrors): Promise<model.Model> {
// Fail hard if the FAIL_HARD env var is defined
const failHard = process.env.FAIL_HARD != null
const initialTypeCount = apiModel.types.length
// Returns the fully-qualified name of a type name
function fqn (name: model.TypeName): string {
return `${name.namespace}:${name.name}`
}
// Are these two type names the same?
function sameTypeName (n1: model.TypeName, n2: model.TypeName): boolean {
return n1.namespace === n2.namespace && n1.name === n2.name
}
function getTypeDef (name: model.TypeName): model.TypeDefinition | undefined {
return typeDefByName.get(fqn(name))
}
// ----- Error logging and collection
// Current endpoint context. Start with an undefined one for common type definitions
let currentEndpoint: string | undefined
let currentPart: 'request'|'response' = 'request'
function setRootContext (endpoint: string, what: 'request' | 'response'): void {
currentEndpoint = endpoint
currentPart = what
}
// Graph traversal context stack
let context: string[] = []
let errorCount = 0
function modelError (msg: string): void {
const fullMsg = (context.length === 0) ? msg : context.join(' / ') + ' - ' + msg
if (currentEndpoint != null) {
errors.addEndpointError(currentEndpoint, currentPart, fullMsg)
} else {
errors.addGeneralError(fullMsg)
}
errorCount++
}
// ----- Type definition management
// Type definitions by name
const typeDefByName = new Map<string, model.TypeDefinition>()
for (const type of apiModel.types) {
typeDefByName.set(fqn(type.name), type)
}
// Behaviors. Filled from the `behaviors` properties from requests and interfaces
// FIXME: to be revisited once it's moved to a top-level api model property
const behaviorByName = new Map<string, model.TypeDefinition>()
// Behaviors by short name
// FIXME: no more needed once we've changed the metamodel to use fqn's
const behaviorByShortName = new Map<string, model.TypeName>()
for (const typeDef of apiModel.types) {
context.push(`Type definition ${fqn(typeDef.name)}`)
if (typeDef.kind === 'request' || typeDef.kind === 'interface') {
for (const bhv of typeDef.behaviors ?? []) {
const bhvDef = getTypeDef(bhv.type)
if (bhvDef == null) {
modelError(`No type definition for '${fqn(bhv.type)}'`)
} else {
behaviorByName.set(fqn(bhv.type), bhvDef)
behaviorByShortName.set(bhv.type.name, bhv.type)
}
}
}
context.pop()
}
// Now remove all behaviors from type definitions
// FIXME: remove once behaviors are a top-level property
apiModel.types = apiModel.types.filter(typeDef => !behaviorByName.has(fqn(typeDef.name)))
// Type definitions that have children (either implements or extends)
const parentTypes = new Set<string>()
for (const type of apiModel.types) {
if (type.kind === 'request' || type.kind === 'interface') {
if (type.inherits != null) {
parentTypes.add(fqn(type.inherits.type))
}
}
}
// Types and behaviors we have actually seen while crawling the reference graph.
// Used both to avoid re-validating types we already visited and to find dangling types at the end.
const typesSeen = new Set<string>()
// ----- Builtin and special types
// Register builtin types
for (const name of [
'string', 'boolean', 'number', 'null', 'void', 'binary'
]) {
const typeName = {
namespace: '_builtins',
name: name
}
typeDefByName.set(fqn(typeName), {
kind: 'interface',
name: typeName,
properties: [],
// arbitrary location, it's not written to schema.json
specLocation: ''
})
}
const additionalRoots: TypeName[] = [
// ErrorResponse is not referenced anywhere, but any API could return it if an error happens.
{ namespace: '_types', name: 'ErrorResponseBase' }
]
additionalRoots.forEach(t => validateTypeRef(t, undefined, new Set()))
// ----- Alright, let's go!
function readyForValidation (ep: model.Endpoint): boolean {
return ep.request != null && ep.response != null
}
// Validate all endpoints. We start by those that are ready for validation so that transitive validation of common
// data types is associated with these endpoints and their errors are not filtered out in the error report.
apiModel.endpoints.filter(ep => readyForValidation(ep)).forEach(validateEndpoint)
apiModel.endpoints.filter(ep => !readyForValidation(ep)).forEach(validateEndpoint)
// Removes types that we've not seen
apiModel.types = apiModel.types.filter(type => typesSeen.has(fqn(type.name)))
// Re-add visited behaviors to model types
for (const [bhn, bh] of behaviorByName) {
if (typesSeen.has(bhn)) {
apiModel.types.push(bh)
}
}
const danglingTypesCount = initialTypeCount - apiModel.types.length
console.info(`Model validation: ${typesSeen.size} types visited, ${danglingTypesCount} dangling types.`)
if (errorCount > 0 && failHard) {
throw new Error('Model is inconsistent. Check logs for details')
}
return apiModel
// -----------------------------------------------------------------------------------------------
/**
* Validate an endpoint
*/
function validateEndpoint (endpoint: model.Endpoint): void {
setRootContext(endpoint.name, 'request')
// Skip validation for internal endpoints
if (privateNamespaces.some(ns => endpoint.name.startsWith(ns))) {
return
}
if (endpoint.request !== null) {
const reqType = getTypeDef(endpoint.request)
if (reqType == null) {
modelError(`Type ${fqn(endpoint.request)} not found`)
} else if (reqType.kind !== 'request') {
modelError(`Type ${fqn(endpoint.request)} is not a request definition`)
} else {
validateTypeDef(reqType)
// Request path properties and url template properties should be the same
const reqProperties = new Set(reqType.path.map(p => p.name))
const urlProperties = new Set<string>()
for (const url of endpoint.urls) {
// Strip trailing slashes from paths
if (url.path !== '/' && url.path.endsWith('/')) {
modelError(`Url path '${url.path}' has a trailing slash`)
url.path = url.path.replace(/\/$/, '')
}
const re = /{([^}]+)}/g
let m
while ((m = re.exec(url.path)) != null) { // eslint-disable-line no-cond-assign
urlProperties.add(m[1])
}
}
for (const urlProp of urlProperties) {
if (!reqProperties.has(urlProp)) {
modelError(`Url path property '${urlProp}' is missing in request definition`)
}
}
for (const reqProp of reqProperties) {
if (!urlProperties.has(reqProp)) {
modelError(`Request path property '${reqProp}' doesn't exist in endpoint URL templates`)
}
}
}
}
setRootContext(endpoint.name, 'response')
if (endpoint.response !== null) {
const respType = getTypeDef(endpoint.response)
if (respType == null) {
modelError(`Type ${fqn(endpoint.response)} not found`)
} else {
validateTypeDef(respType)
}
}
}
/**
* Validate a type reference.
*
* @param name the type's name
* @param generics generic parameter for this reference
* @param openGenerics fully qualified names of open generic parameters in the enclosing scope
* @param kind what do we expect this type to be?
*/
function validateTypeRef (name: model.TypeName, generics: (model.ValueOf[] | undefined), openGenerics: Set<string>, kind: TypeDefKind = TypeDefKind.type): void {
const fqName = fqn(name)
if (openGenerics.has(fqName)) {
// This is an open generic parameter coming from the enclosing scope
return
}
const typeDef = kind === TypeDefKind.behavior ? behaviorByName.get(fqName) : getTypeDef(name)
if (typeDef == null) {
modelError(`No type definition for '${fqName}'`)
return
}
if (typeDef.kind === 'request') {
modelError(`'${fqName}' is a request and must only be used in endpoints`)
}
validateTypeDef(typeDef)
// Validate generic parameters
const genericsCount = generics?.length ?? 0
if (typeDef.kind !== 'enum') {
const typeGenericsCount = typeDef.generics?.length ?? 0
if (genericsCount !== typeGenericsCount) {
modelError(`Expected ${typeGenericsCount} generic parameters but got ${genericsCount}`)
}
} else {
if (genericsCount !== 0) {
modelError(`Type kind '${typeDef.kind}' doesn't accept generic parameters`)
}
}
context.push('Generics')
for (const v of generics ?? []) {
validateValueOf(v, openGenerics)
}
context.pop()
}
// -----------------------------------------------------------------------------------------------
// Type definitions validations
function validateTypeDef (typeDef: model.TypeDefinition): void {
if (typesSeen.has(fqn(typeDef.name))) {
// Already visited
return
}
typesSeen.add(fqn(typeDef.name))
// Start a new context, type definitions are top-level elements
const oldContext = context
context = []
context.push(`${typeDef.kind} definition ${typeDef.name.namespace}:${typeDef.name.name}`)
// Validate description URL
if (typeDef.docUrl != null) {
try {
new URL(typeDef.docUrl) // eslint-disable-line no-new
} catch (error) {
modelError('Description URL is malformed')
}
}
switch (typeDef.kind) {
case 'request':
validateRequest(typeDef)
break
case 'response':
validateResponse(typeDef)
break
case 'interface':
validateInterface(typeDef)
break
case 'enum':
validateEnum(typeDef)
break
case 'type_alias':
validateTypeAlias(typeDef)
break
default:
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown kind: ${typeDef.kind}`)
}
// Restore previous context
context = oldContext
}
function validateRequest (typeDef: model.Request): void {
const openGenerics = openGenericSet(typeDef)
validateInherits(typeDef.inherits, openGenerics)
validateBehaviors(typeDef, openGenerics)
// Note: we validate codegen_name/name uniqueness independently in the path, query and body as there are some
// valid overlaps, with some parameters that can also be represented as body properties.
// Client generators will have to take care of this.
const inheritedProps = inheritedProperties(typeDef)
context.push('path')
validateProperties(typeDef.path, openGenerics, inheritedProps)
context.pop()
context.push('query')
validateProperties(typeDef.query, openGenerics, inheritedProps)
context.pop()
context.push('body')
switch (typeDef.body.kind) {
case 'properties':
validateProperties(typeDef.body.properties, openGenerics, inheritedProps)
break
case 'value':
validateValueOf(typeDef.body.value, openGenerics)
break
case 'no_body':
// Nothing to validate
break
default:
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
}
context.pop()
}
function validateResponse (typeDef: model.Response): void {
const openGenerics = openGenericSet(typeDef)
// Note: we validate codegen_name/name uniqueness independently in the path, query and body as there are some
// valid overlaps, with some parameters that can also be represented as body properties.
// Client generators will have to take care of this.
context.push('body')
switch (typeDef.body.kind) {
case 'properties':
validateProperties(typeDef.body.properties, openGenerics, inheritedProperties(typeDef))
break
case 'value':
validateValueOf(typeDef.body.value, openGenerics)
break
case 'no_body':
// Nothing to validate
break
default:
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
}
context.pop()
}
/** Collects the name of inherited properties. Names are normalized to lower case (see also validateProperties) */
function inheritedProperties (typeDef: model.Interface | model.Request | model.Response, accum?: Set<string>): Set<string> {
const result = accum ?? new Set<string>()
function addProperties (props: model.Property[]): void {
props.forEach(prop => result.add((prop.codegenName ?? prop.name).toLowerCase()))
}
function addInherits (inherits?: model.Inherits): void {
if (inherits == null) {
return
}
const typeDef = getTypeDef(inherits.type)
if (typeDef == null) {
modelError(`No type definition for parent '${fqn(inherits.type)}'`)
return
}
switch (typeDef?.kind) {
case 'request':
inheritedProperties(typeDef, result)
addProperties(typeDef.path)
addProperties(typeDef.query)
if (typeDef.body.kind === 'properties') {
addProperties(typeDef.body.properties)
}
break
case 'response':
inheritedProperties(typeDef, result)
if (typeDef.body.kind === 'properties') {
addProperties(typeDef.body.properties)
}
break
case 'interface':
inheritedProperties(typeDef, result)
addProperties(typeDef.properties)
break
}
}
if (typeDef.kind !== 'response') {
if (typeDef.inherits != null) {
addInherits(typeDef.inherits)
}
typeDef.behaviors?.forEach(addInherits)
}
return result
}
function validateInterface (typeDef: model.Interface): void {
const openGenerics = openGenericSet(typeDef)
validateInherits(typeDef.inherits, openGenerics)
validateBehaviors(typeDef, openGenerics)
validateProperties(typeDef.properties, openGenerics, inheritedProperties(typeDef))
if (typeDef.variants?.kind === 'container') {
const variants = typeDef.properties.filter(prop => !(prop.containerProperty ?? false))
if (variants.length === 1) {
// Single-variant containers must have a required property
if (!variants[0].required) {
modelError(`Property ${variants[0].name} is a single-variant and must be required`)
}
} else {
// Multiple variants must all be optional
for (const v of variants) {
if (v.required) {
modelError(`Variant ${variants[0].name} must be optional`)
}
}
}
}
}
function validateEnum (typeDef: model.Enum): void {
const allIdentifiers = new Set<string>()
const allNames = new Set<string>()
for (const item of typeDef.members) {
// Identifier must be unique among all items (case insensitive)
const codegenName = (item.codegenName ?? item.name).toLowerCase()
if (allIdentifiers.has(codegenName)) {
modelError(`Duplicate enum member codegen_name '${item.name}'`)
}
allIdentifiers.add(codegenName)
// Name must be unique among all items (case sensitive)
if (allNames.has(item.name)) {
modelError(`Duplicate enum member name '${item.name}'`)
}
allNames.add(item.name)
}
}
function validateTypeAlias (typeDef: model.TypeAlias): void {
const openGenerics = openGenericSet(typeDef)
if (typeDef.variants != null) {
if (typeDef.type.kind !== 'union_of') {
modelError('The "variants" tag only applies to unions')
} else {
validateTaggedUnion(typeDef.name, typeDef.type, typeDef.variants, openGenerics)
}
} else {
validateValueOf(typeDef.type, openGenerics)
}
}
function validateTaggedUnion (parentName: TypeName, valueOf: model.UnionOf, variants: model.InternalTag | model.ExternalTag | model.Untagged, openGenerics: Set<string>): void {
if (variants.kind === 'external_tag') {
// All items must have a 'variant' attribute
const items = flattenUnionMembers(valueOf, openGenerics)
for (const item of items) {
if (item.kind !== 'instance_of') {
modelError('Items of externally tagged unions must be types with a "variant_tag" annotation')
} else {
validateTypeRef(item.type, item.generics, openGenerics)
const type = getTypeDef(item.type)
if (type == null) {
modelError(`Type ${fqn(item.type)} not found`)
} else {
if (type.variantName == null) {
modelError(`Type ${fqn(item.type)} is part of a tagged union and should have a "@variant name"`)
} else {
// Check uniqueness
}
}
}
}
} else if (variants.kind === 'internal_tag') {
const tagName = variants.tag
const items = flattenUnionMembers(valueOf, openGenerics)
for (const item of items) {
if (item.kind !== 'instance_of') {
modelError('Items of internally tagged unions must be type references')
} else {
validateTypeRef(item.type, item.generics, openGenerics)
const type = getTypeDef(item.type)
if (type == null) {
modelError(`Type ${fqn(item.type)} not found`)
} else if (type.kind === 'interface') {
const tagProperty = type.properties.find(prop => prop.name === tagName)
if (tagProperty == null) {
modelError(`Type ${fqn(item.type)} should have a "${tagName}" variant tag property`)
}
}
}
}
validateValueOf(valueOf, openGenerics)
} else if (variants.kind === 'untagged') {
if (fqn(parentName) !== '_types.query_dsl:DecayFunction' &&
fqn(parentName) !== '_types.query_dsl:DistanceFeatureQuery' &&
fqn(parentName) !== '_types.query_dsl:RangeQuery') {
throw new Error(`Please contact the devtools team before adding new untagged variant ${fqn(parentName)}`)
}
const untypedVariant = getTypeDef(variants.untypedVariant)
if (untypedVariant == null) {
modelError(`Type ${fqn(variants.untypedVariant)} not found`)
}
const items = flattenUnionMembers(valueOf, openGenerics)
const baseTypes = new Set<string>()
let foundUntyped = false
for (const item of items) {
if (item.kind !== 'instance_of') {
modelError('Items of type untagged unions must be type references')
} else {
validateTypeRef(item.type, item.generics, openGenerics)
const type = getTypeDef(item.type)
if (type == null) {
modelError(`Type ${fqn(item.type)} not found`)
} else {
if (type.kind !== 'interface') {
modelError(`Type ${fqn(item.type)} must be an interface to be used in an untagged union`)
continue
}
if (untypedVariant != null && fqn(item.type) === fqn(untypedVariant.name)) {
foundUntyped = true
}
if (type.inherits == null) {
modelError(`Type ${fqn(item.type)} must derive from a base type to be used in an untagged union`)
continue
}
baseTypes.add(fqn(type.inherits.type))
const baseType = getTypeDef(type.inherits.type)
if (baseType == null) {
modelError(`Type ${fqn(type.inherits.type)} not found`)
continue
}
if (baseType.kind !== 'interface') {
modelError(`Type ${fqn(type.inherits.type)} must be an interface to be used as the base class of another interface`)
continue
}
if (baseType.generics == null || baseType.generics.length === 0) {
modelError('The common base type of an untagged union must accept at least one generic type argument')
}
}
}
}
if (baseTypes.size !== 1) {
modelError('All items of an untagged union must derive from the same common base type')
}
if (!foundUntyped) {
modelError('The untyped variant of an untagged variant must be contained in the union items')
}
}
}
// -----------------------------------------------------------------------------------------------
// Constituents of type definitions
function openGenericSet (typeDef: model.Request | model.Response | model.Interface | model.TypeAlias): Set<string> {
return new Set((typeDef.generics ?? []).map(name => fqn(name)))
}
function validateInherits (parent: (model.Inherits | undefined), openGenerics: Set<string>): void {
if (parent == null) return
context.push('Inherits')
validateTypeRef(parent.type, parent.generics, openGenerics)
context.pop()
}
function validateBehaviors (typeDef: model.Request | model.Response | model.Interface, openGenerics: Set<string>): void {
if (typeDef.kind !== 'response' && typeDef.behaviors != null && typeDef.behaviors.length > 0) {
context.push('Behaviors')
for (const parent of typeDef.behaviors) {
validateTypeRef(parent.type, parent.generics, openGenerics)
if (parent.type.name === 'AdditionalProperty' && (parent.meta == null || parent.meta.key == null || parent.meta.value == null)) {
modelError(`AdditionalProperty behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with at least 2 arguments (key, value)`)
}
if (parent.type.name === 'AdditionalProperties' && (parent.meta == null || parent.meta.fieldname == null || parent.meta.description == null)) {
modelError(`AdditionalProperties behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with exactly 2 arguments (fieldname, description)`)
}
}
context.pop()
}
// Each attached behavior must be in the behaviors attached to this class or an ancestor
if (typeDef.kind !== 'response' && typeDef.attachedBehaviors != null) {
for (const abh of typeDef.attachedBehaviors) {
const bhName = behaviorByShortName.get(abh)
if (bhName == null) {
modelError(`No type definition for behavior '${abh}'`)
continue
}
if (!behaviorExists(typeDef, bhName)) {
modelError(`Attached behavior '${abh}' not found in type or ancestors`)
}
}
}
}
/** Recursively looks up a behavior in a type definition or its ancestors */
function behaviorExists (type: (model.Request | model.Interface), behavior: model.TypeName): boolean {
// Does the type have this behavior?
for (const b of (type.behaviors ?? [])) {
if (sameTypeName(b.type, behavior)) {
return true
}
}
// Does a parent have this behavior?
if (type.inherits != null) {
const parentDef = getTypeDef(type.inherits.type)
if (parentDef == null) {
modelError(`No type definition for parent '${fqn(type.inherits.type)}'`)
return false
}
if (parentDef.kind === 'request' || parentDef.kind === 'interface') {
if (behaviorExists(parentDef, behavior)) {
return true
}
}
}
return false
}
function validateProperties (props: model.Property[], openGenerics: Set<string>, inheritedProperties: Set<string>): void {
const allIdentifiers = new Set<string>()
const allNames = new Set<string>()
for (const prop of props) {
// Identifier must be unique among all items (case insensitive)
const codegenName = (prop.codegenName ?? prop.name).toLowerCase()
if (allIdentifiers.has(codegenName)) {
modelError(`Duplicate property codegen_name: '${prop.name}'`)
}
allIdentifiers.add(codegenName)
if (inheritedProperties.has(codegenName)) {
modelError(`Property '${prop.name}' is already defined in an ancestor class`)
}
// Names and aliases must be unique among all items (case sensitive)
const names = [prop.name].concat(prop.aliases ?? [])
for (const name of names) {
if (allNames.has(name)) {
modelError(`Duplicate property name or alias: '${name}'`)
}
allNames.add(name)
}
context.push(`Property '${prop.name}'`)
validateValueOf(prop.type, openGenerics)
validateValueOfJsonEvents(prop.type)
context.pop()
}
}
// -----------------------------------------------------------------------------------------------
// Value_Of validations
function validateValueOf (valueOf: model.ValueOf, openGenerics: Set<string>): void {
context.push(valueOf.kind)
switch (valueOf.kind) {
case 'instance_of':
validateTypeRef(valueOf.type, valueOf.generics, openGenerics)
break
case 'array_of':
validateValueOf(valueOf.value, openGenerics)
break
case 'union_of':
for (const item of valueOf.items) {
validateValueOf(item, openGenerics)
}
break
case 'dictionary_of':
validateValueOf(valueOf.key, openGenerics)
validateValueOf(valueOf.value, openGenerics)
break
case 'user_defined_value':
// Nothing to validate
break
case 'literal_value':
// Nothing to validate
break
default:
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown kind: ${valueOf.kind}`)
}
context.pop()
}
// -----------------------------------------------------------------------------------------------
// JSON events validation
function validateValueOfJsonEvents (valueOf: model.ValueOf): void {
// TODO: disabled for now as it's too noisy until we have fully annotated variants
// const events = new Set<JsonEvent>()
// valueOfJsonEvents(events, valueOf)
}
function validateEvent (events: Set<JsonEvent>, event: JsonEvent): void {
if (events.has(event)) {
modelError('Ambiguous JSON type: ' + event)
}
events.add(event)
}
function typeDefJsonEvents (events: Set<JsonEvent>, typeDef: model.TypeDefinition): void {
if (typeDef.name.namespace === '_builtins') {
switch (typeDef.name.name) {
case 'string':
validateEvent(events, JsonEvent.string)
return
case 'boolean':
validateEvent(events, JsonEvent.boolean)
return
case 'number':
validateEvent(events, JsonEvent.number)
return
case 'object':
validateEvent(events, JsonEvent.object)
return
case 'null':
validateEvent(events, JsonEvent.null)
return
case 'Array':
validateEvent(events, JsonEvent.array)
return
}
}
switch (typeDef.kind) {
case 'request':
case 'interface':
validateEvent(events, JsonEvent.object)
break
case 'enum':
validateEvent(events, JsonEvent.string)
break
case 'type_alias':
if (typeDef.variants == null) {
valueOfJsonEvents(events, typeDef.type)
} else {
// tagged union: the discriminant tells us what to look for, check each member in isolation
assert(typeDef.type.kind === 'union_of', 'Variants are only allowed on union_of type aliases')
for (const item of flattenUnionMembers(typeDef.type, new Set())) {
validateValueOfJsonEvents(item)
}
// Internally tagged variants will be objects, so check that we can read them
if (typeDef.variants.kind === 'internal_tag') {
// variant items will be objects
validateEvent(events, JsonEvent.object)
}
}
break
}
}
/** Build the flattened item list of potentially nested unions (this is used for large unions) */
function flattenUnionMembers (union: model.UnionOf, openGenerics: Set<string>): model.ValueOf[] {
const allItems = new Array<model.ValueOf>()
function collectItems (items: model.ValueOf[]): void {
for (const item of items) {
if (item.kind !== 'instance_of') {
validateValueOf(item, openGenerics)
allItems.push(item)
} else {
const itemType = getTypeDef(item.type)
if (itemType?.kind === 'type_alias' && itemType.type.kind === 'union_of') {
// Mark it as seen
typesSeen.add(fqn(item.type))
// Recurse in nested union
collectItems(itemType.type.items)
} else {
validateValueOf(item, openGenerics)
allItems.push(item)
}
}
}
}
collectItems(union.items)
return allItems
}
function typeRefJsonEvents (events: Set<JsonEvent>, name: model.TypeName): void {
const type = getTypeDef(name)
if (type != null) {
typeDefJsonEvents(events, type)
}
}
function valueOfJsonEvents (events: Set<JsonEvent>, valueOf: model.ValueOf): void {
context.push(valueOf.kind)
switch (valueOf.kind) {
case 'instance_of':
typeRefJsonEvents(events, valueOf.type)
break
case 'user_defined_value':
validateEvent(events, JsonEvent.object) // but can be anything
break
case 'array_of':
validateEvent(events, JsonEvent.array)
validateValueOfJsonEvents(valueOf.value)
break
case 'dictionary_of':
validateEvent(events, JsonEvent.object)
context.push('key')
validateValueOfJsonEvents(valueOf.key)
context.pop()
context.push('value')
validateValueOfJsonEvents(valueOf.value)
context.pop()
break
case 'union_of':
for (const item of valueOf.items) {
valueOfJsonEvents(events, item)
}
break
}
context.pop()
}
}