in compiler/src/model/build-model.ts [167:568]
function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | InterfaceDeclaration, mappings: Record<string, model.Endpoint>, allClasses: ClassDeclaration[]): model.Request | model.Response | model.Interface {
const name = declaration.getName()
assert(declaration, name != null, 'Anonymous definitions should not exists')
if (name === 'Request') {
assert(
declaration,
Node.isInterfaceDeclaration(declaration),
`Request definitions must be declared as interfaces: ${name}`
)
}
if (name === 'Response') {
assert(
declaration,
Node.isClassDeclaration(declaration),
`Response definitions must be declared as classes: ${name}`
)
}
// Request and Response definitions needs to be handled
// differently from normal classes
if (name === 'Request' || name === 'Response') {
let type: model.Request | model.Response
const namespace = getNameSpace(declaration)
if (name === 'Request') {
type = {
specLocation: sourceLocation(declaration),
kind: 'request',
name: { name, namespace },
path: new Array<model.Property>(),
query: new Array<model.Property>(),
body: { kind: 'no_body' }
}
const response: model.TypeName = {
name: 'Response',
namespace: getNameSpace(declaration)
}
hoistRequestAnnotations(type, declaration.getJsDocs(), mappings, response)
const mapping = mappings[namespace.includes('_global') ? namespace.slice(8) : namespace]
if (mapping == null) {
throw new Error(`Cannot find url template for ${namespace}, very likely the specification folder does not follow the rest-api-spec`)
}
let pathMember: Node | null = null
let bodyProperties: model.Property[] = []
let bodyValue: model.ValueOf | null = null
let bodyMember: Node | null = null
// collect path/query/body properties
for (const member of declaration.getMembers()) {
// we are visiting `urls`, `path_parts, `query_parameters` or `body`
assert(
member,
Node.isPropertyDeclaration(member) || Node.isPropertySignature(member),
'Class and interfaces can only have property declarations or signatures'
)
const name = member.getName()
if (name === 'urls') {
// Overwrite the endpoint urls read from the json-rest-spec
// TODO: once all spec files are using it, make it mandatory.
mapping.urls = visitUrls(member)
} else if (name === 'path_parts') {
const property = visitRequestOrResponseProperty(member)
assert(member, property.properties.length > 0, 'There is no need to declare an empty object path_parts, just remove the path_parts declaration.')
pathMember = member
type.path = property.properties
} else if (name === 'query_parameters') {
const property = visitRequestOrResponseProperty(member)
assert(member, property.properties.length > 0, 'There is no need to declare an empty object query_parameters, just remove the query_parameters declaration.')
type.query = property.properties
} else if (name === 'body') {
const property = visitRequestOrResponseProperty(member)
bodyMember = member
if (property.valueOf != null) {
bodyValue = property.valueOf
} else {
assert(member, property.properties.length > 0, 'There is no need to declare an empty object body, just remove the body declaration.')
bodyProperties = property.properties
}
} else {
assert(member, false, `Unknown request property: ${name}`)
}
}
// validate path properties
// list of unique dynamic parameters
const urlTemplateParams = [...new Set(
mapping.urls.flatMap(url => url.path.split('/')
.filter(part => part.includes('{'))
.map(part => part.slice(1, -1))
)
)]
const methods = [...new Set(mapping.urls.flatMap(url => url.methods))]
for (const part of type.path) {
assert(
pathMember as Node,
urlTemplateParams.includes(part.name),
`The property '${part.name}' does not exist in the rest-api-spec ${namespace} url template`
)
if (type.query.map(p => p.name).includes(part.name)) {
const queryType = type.query.find(property => property != null && property.name === part.name) as model.Property
if (!deepEqual(queryType.type, part.type)) {
assert(pathMember as Node, part.codegenName != null, `'${part.name}' already exist in the query_parameters with a different type, you should define an @codegen_name.`)
assert(pathMember as Node, !type.query.map(p => p.codegenName ?? p.name).includes(part.codegenName), `The codegen_name '${part.codegenName}' already exists as parameter in query_parameters.`)
}
}
if (bodyProperties.map(p => p.name).includes(part.name)) {
const bodyType = bodyProperties.find(property => property != null && property.name === part.name) as model.Property
if (!deepEqual(bodyType.type, part.type)) {
assert(pathMember as Node, part.codegenName != null, `'${part.name}' already exist in the body with a different type, you should define an @codegen_name.`)
assert(pathMember as Node, !bodyProperties.map(p => p.codegenName ?? p.name).includes(part.codegenName), `The codegen_name '${part.codegenName}' already exists as parameter in body.`)
}
}
}
// validate body
if (bodyMember != null) {
assert(
bodyMember,
methods.some(method => ['POST', 'PUT', 'DELETE'].includes(method)),
`${namespace}.${name} can't have a body, allowed methods: ${methods.join(', ')}`
)
}
// the body can either be a value (eg Array<string> or an object with properties)
if (bodyValue != null) {
// Propagate required body value nature based on TS question token being present.
// Overrides the value set by spec files.
mapping.requestBodyRequired = !(bodyMember as PropertySignature).hasQuestionToken()
if (bodyValue.kind === 'instance_of' && bodyValue.type.name === 'Void') {
assert(bodyMember as Node, false, 'There is no need to use Void in requets definitions, just remove the body declaration.')
} else {
const tags = parseJsDocTags((bodyMember as PropertySignature).getJsDocs())
assert(
bodyMember as Node,
tags.codegen_name != null,
'You should configure a body @codegen_name'
)
assert(
(bodyMember as PropertySignature).getJsDocs(),
!type.path.map(p => p.codegenName ?? p.name).concat(type.query.map(p => p.codegenName ?? p.name)).includes(tags.codegen_name),
`The codegen_name '${tags.codegen_name}' already exists as a property in the path or query.`
)
type.body = {
kind: 'value',
value: bodyValue,
codegenName: tags.codegen_name
}
}
} else if (bodyProperties.length > 0) {
type.body = { kind: 'properties', properties: bodyProperties }
}
// The interface is extended, an extended interface could accept generics as well,
// Implements will be caught here as well, they can be differentiated by looking as `node.token`
// which can either be `ts.SyntaxKind.ExtendsKeyword` or `ts.SyntaxKind.ImplementsKeyword`
// In case of `ts.SyntaxKind.ImplementsKeyword`, we need to check
// if it's a normal implements or a behavior, in such case, the behaviors
// need to be collected and added to the type.
if (declaration.getHeritageClauses().length > 0) {
// check if the current node or one of the ancestor
// has one or more behaviors attached
const attachedBehaviors = getAllBehaviors(declaration)
if (attachedBehaviors.length > 0) {
type.attachedBehaviors = Array.from(
new Set((type.attachedBehaviors ?? []).concat(attachedBehaviors))
)
}
}
for (const inherit of declaration.getHeritageClauses()) {
const extended = inherit.getTypeNodes()
.map(t => t.getExpression())
.map(t => t.getType().getSymbol()?.getDeclarations()[0])[0]
assert(inherit, Node.isClassDeclaration(extended) || Node.isInterfaceDeclaration(extended), 'Should extend from a class or interface')
type.inherits = modelInherits(extended, inherit)
}
// If the body wasn't set and we have a parent class, then it's a property body with no additional properties
if (type.body.kind === 'no_body' && type.inherits != null) {
const parent = type.inherits.type
// RequestBase is special as it's a "marker" base class that doesn't imply a property body type. We should get rid of it.
if (parent.name === 'RequestBase' && parent.namespace === '_types') {
// nothing to do
// CatRequestBase is special as it's a "marker" base class that doesn't imply a property body type. We should get rid of it.
} else if (parent.name === 'CatRequestBase' && parent.namespace === 'cat._types') {
// nothing to do
} else {
type.body = { kind: 'properties', properties: new Array<model.Property>() }
}
}
} else {
type = {
specLocation: sourceLocation(declaration),
kind: 'response',
name: { name, namespace: getNameSpace(declaration) },
body: { kind: 'no_body' }
}
for (const member of declaration.getMembers()) {
// we are visiting `path_parts, `query_parameters` or `body`
assert(
member,
Node.isPropertyDeclaration(member) || Node.isPropertySignature(member),
'Class and interfaces can only have property declarations or signatures'
)
if (member.getName() === 'body') {
const property = visitRequestOrResponseProperty(member)
// the body can either by a value (eg Array<string> or an object with properties)
if (property.valueOf != null) {
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
type.body = { kind: 'no_body' }
} else {
const tags = parseJsDocTags((member as PropertySignature).getJsDocs())
assert(
member as Node,
tags.codegen_name != null,
'You should configure a body @codegen_name'
)
type.body = {
kind: 'value',
value: property.valueOf,
codegenName: tags.codegen_name
}
}
} else {
type.body = { kind: 'properties', properties: property.properties }
}
} else if (member.getName() === 'exceptions') {
const exceptions: model.ResponseException[] = []
const property = member.getTypeNode()
assert(
property,
Node.isTupleTypeNode(property),
'Exceptionlures should be an array.'
)
for (const element of property.getElements()) {
const exception: model.ResponseException = {
statusCodes: [],
body: { kind: 'no_body' }
}
element.forEachChild(child => {
assert(
child,
Node.isPropertySignature(child) || Node.isPropertyDeclaration(child),
`Children should be ${ts.SyntaxKind[ts.SyntaxKind.PropertySignature]} or ${ts.SyntaxKind[ts.SyntaxKind.PropertyDeclaration]} but is ${ts.SyntaxKind[child.getKind()]} instead`
)
const jsDocs = child.getJsDocs()
if (jsDocs.length > 0) {
exception.description = jsDocs[0].getDescription().replace(/\r/g, '')
}
if (child.getName() === 'statusCodes') {
const value = child.getTypeNode()
assert(value, Node.isTupleTypeNode(value), 'statusCodes should be an array.')
for (const code of value.getElements()) {
assert(code, Node.isLiteralTypeNode(code) && Number.isInteger(Number(code.getText())), 'Status code values should a valid integer')
assert(code, STATUS_CODES[code.getText()] != null, `${code.getText()} is not a valid status code`)
exception.statusCodes.push(Number(code.getText()))
}
} else if (child.getName() === 'body') {
const property = visitRequestOrResponseProperty(child)
// the body can either by a value (eg Array<string> or an object with properties)
if (property.valueOf != null) {
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
exception.body = { kind: 'no_body' }
} else {
exception.body = { kind: 'value', value: property.valueOf }
}
} else {
exception.body = { kind: 'properties', properties: property.properties }
}
} else {
assert(child, false, 'Exception.body and Exception.statusCode are the only Exception properties supported')
}
})
exceptions.push(exception)
}
type.exceptions = exceptions
} else {
assert(member, false, 'Response.body and Response.exceptions are the only Response properties supported')
}
}
assert(
declaration,
declaration.getHeritageClauses().length === 0,
'Responses cannot be extended'
)
}
for (const typeParameter of declaration.getTypeParameters()) {
type.generics = (type.generics ?? []).concat({
name: modelGenerics(typeParameter),
namespace: type.name.namespace + '.' + type.name.name
})
}
return type
// Every other class or interface will be handled here
} else {
const type: model.Interface = {
specLocation: sourceLocation(declaration),
kind: 'interface',
name: { name, namespace: getNameSpace(declaration) },
properties: new Array<model.Property>()
}
const jsDocs = declaration.getJsDocs()
hoistTypeAnnotations(type, jsDocs)
const variant = parseVariantNameTag(declaration.getJsDocs())
if (typeof variant === 'string') {
type.variantName = variant
}
const variants = parseVariantsTag(declaration.getJsDocs())
if (variants != null) {
assert(declaration.getJsDocs(), variants.kind === 'container', 'Interfaces can only use `container` variant kind')
type.variants = variants
}
for (const member of declaration.getMembers()) {
// Any property definition
assert(
member,
Node.isPropertyDeclaration(member) || Node.isPropertySignature(member),
'Class and interfaces can only have property declarations or signatures'
)
try {
const property = modelProperty(member)
if (type.variants?.kind === 'container' && property.containerProperty == null) {
assert(
member,
!property.required,
'All @variants container properties must be optional'
)
}
type.properties.push(property)
} catch (e) {
const name = declaration.getName()
if (name !== undefined) {
console.log(`failed to parse ${name}, reason:`, e.message)
} else {
console.log('failed to parse field, reason:', e.message)
}
process.exit(1)
}
}
// The class or interface is extended, an extended class or interface could
// accept generics as well, Implements will be caught here as well,
// they can be differentiated by looking as `node.token`, which can either be
// `ts.SyntaxKind.ExtendsKeyword` or `ts.SyntaxKind.ImplementsKeyword`
// In case of `ts.SyntaxKind.ImplementsKeyword`, we need to check
// if it's a normal implements or a behavior, in such case, the behaviors
// need to be collected and added to the type.
if (declaration.getHeritageClauses().length > 0) {
// check if the current node or one of the ancestor
// has one or more behaviors attached
const attachedBehaviors = getAllBehaviors(declaration)
if (attachedBehaviors.length > 0) {
type.attachedBehaviors = Array.from(
new Set((type.attachedBehaviors ?? []).concat(attachedBehaviors))
)
}
}
for (const inherit of declaration.getHeritageClauses()) {
if (inherit.getToken() === ts.SyntaxKind.ExtendsKeyword) {
const extended = inherit.getTypeNodes()
.map(t => t.getExpression())
.map(t => t.getType().getSymbol()?.getDeclarations()[0])[0]
assert(inherit, Node.isClassDeclaration(extended) || Node.isInterfaceDeclaration(extended), 'Should extend from a class or interface')
type.inherits = modelInherits(extended, inherit)
}
}
// Only classes can implement interfaces
if (Node.isClassDeclaration(declaration)) {
for (const implement of declaration.getImplements()) {
if (isKnownBehavior(implement)) {
type.behaviors = (type.behaviors ?? []).concat(modelBehaviors(implement, jsDocs))
}
}
}
for (const typeParameter of declaration.getTypeParameters()) {
type.generics = (type.generics ?? []).concat({
name: modelGenerics(typeParameter),
namespace: type.name.namespace + '.' + type.name.name
})
}
return type
}