diff --git a/lib/src/generators/contract/__snapshots__/json-schema.spec.ts.snap b/lib/src/generators/contract/__snapshots__/json-schema.spec.ts.snap index 36310f76d..09133aa47 100644 --- a/lib/src/generators/contract/__snapshots__/json-schema.spec.ts.snap +++ b/lib/src/generators/contract/__snapshots__/json-schema.spec.ts.snap @@ -31,6 +31,32 @@ exports[`JSON Schema generator produces valid code: json 1`] = ` \\"data\\" ] }, + \\"Profile\\": { + \\"type\\": \\"object\\", + \\"properties\\": { + \\"private\\": { + \\"type\\": \\"boolean\\" + }, + \\"messageOptions\\": { + \\"$ref\\": \\"#/definitions/MessageOptions\\" + } + }, + \\"required\\": [ + \\"private\\", + \\"messageOptions\\" + ] + }, + \\"MessageOptions\\": { + \\"type\\": \\"object\\", + \\"properties\\": { + \\"newsletter\\": { + \\"type\\": \\"boolean\\" + } + }, + \\"required\\": [ + \\"newsletter\\" + ] + }, \\"ErrorBody\\": { \\"type\\": \\"object\\", \\"properties\\": { @@ -84,32 +110,6 @@ exports[`JSON Schema generator produces valid code: json 1`] = ` \\"data\\" ] }, - \\"Profile\\": { - \\"type\\": \\"object\\", - \\"properties\\": { - \\"private\\": { - \\"type\\": \\"boolean\\" - }, - \\"messageOptions\\": { - \\"$ref\\": \\"#/definitions/MessageOptions\\" - } - }, - \\"required\\": [ - \\"private\\", - \\"messageOptions\\" - ] - }, - \\"MessageOptions\\": { - \\"type\\": \\"object\\", - \\"properties\\": { - \\"newsletter\\": { - \\"type\\": \\"boolean\\" - } - }, - \\"required\\": [ - \\"newsletter\\" - ] - }, \\"Email\\": { \\"type\\": \\"string\\" }, @@ -141,6 +141,23 @@ definitions: - profile required: - data + Profile: + type: object + properties: + private: + type: boolean + messageOptions: + $ref: '#/definitions/MessageOptions' + required: + - private + - messageOptions + MessageOptions: + type: object + properties: + newsletter: + type: boolean + required: + - newsletter ErrorBody: type: object properties: @@ -177,23 +194,6 @@ definitions: - address required: - data - Profile: - type: object - properties: - private: - type: boolean - messageOptions: - $ref: '#/definitions/MessageOptions' - required: - - private - - messageOptions - MessageOptions: - type: object - properties: - newsletter: - type: boolean - required: - - newsletter Email: type: string Address: diff --git a/lib/src/generators/contract/__snapshots__/openapi2.spec.ts.snap b/lib/src/generators/contract/__snapshots__/openapi2.spec.ts.snap index d586ae3e0..6a8730620 100644 --- a/lib/src/generators/contract/__snapshots__/openapi2.spec.ts.snap +++ b/lib/src/generators/contract/__snapshots__/openapi2.spec.ts.snap @@ -145,7 +145,10 @@ exports[`OpenAPI 2 generator produces valid code: json 1`] = ` } }, \\"definitions\\": { - \\"UserBody\\": { + \\"Address\\": { + \\"type\\": \\"string\\" + }, + \\"CreateUserRequestBody\\": { \\"type\\": \\"object\\", \\"properties\\": { \\"data\\": { @@ -157,14 +160,22 @@ exports[`OpenAPI 2 generator produces valid code: json 1`] = ` \\"lastName\\": { \\"type\\": \\"string\\" }, - \\"profile\\": { - \\"$ref\\": \\"#/definitions/Profile\\" + \\"age\\": { + \\"type\\": \\"number\\" + }, + \\"email\\": { + \\"$ref\\": \\"#/definitions/Email\\" + }, + \\"address\\": { + \\"$ref\\": \\"#/definitions/Address\\" } }, \\"required\\": [ \\"firstName\\", \\"lastName\\", - \\"profile\\" + \\"age\\", + \\"email\\", + \\"address\\" ] } }, @@ -172,6 +183,9 @@ exports[`OpenAPI 2 generator produces valid code: json 1`] = ` \\"data\\" ] }, + \\"Email\\": { + \\"type\\": \\"string\\" + }, \\"ErrorBody\\": { \\"type\\": \\"object\\", \\"properties\\": { @@ -190,39 +204,15 @@ exports[`OpenAPI 2 generator produces valid code: json 1`] = ` \\"message\\" ] }, - \\"CreateUserRequestBody\\": { + \\"MessageOptions\\": { \\"type\\": \\"object\\", \\"properties\\": { - \\"data\\": { - \\"type\\": \\"object\\", - \\"properties\\": { - \\"firstName\\": { - \\"type\\": \\"string\\" - }, - \\"lastName\\": { - \\"type\\": \\"string\\" - }, - \\"age\\": { - \\"type\\": \\"number\\" - }, - \\"email\\": { - \\"$ref\\": \\"#/definitions/Email\\" - }, - \\"address\\": { - \\"$ref\\": \\"#/definitions/Address\\" - } - }, - \\"required\\": [ - \\"firstName\\", - \\"lastName\\", - \\"age\\", - \\"email\\", - \\"address\\" - ] + \\"newsletter\\": { + \\"type\\": \\"boolean\\" } }, \\"required\\": [ - \\"data\\" + \\"newsletter\\" ] }, \\"Profile\\": { @@ -240,22 +230,32 @@ exports[`OpenAPI 2 generator produces valid code: json 1`] = ` \\"messageOptions\\" ] }, - \\"MessageOptions\\": { + \\"UserBody\\": { \\"type\\": \\"object\\", \\"properties\\": { - \\"newsletter\\": { - \\"type\\": \\"boolean\\" + \\"data\\": { + \\"type\\": \\"object\\", + \\"properties\\": { + \\"firstName\\": { + \\"type\\": \\"string\\" + }, + \\"lastName\\": { + \\"type\\": \\"string\\" + }, + \\"profile\\": { + \\"$ref\\": \\"#/definitions/Profile\\" + } + }, + \\"required\\": [ + \\"firstName\\", + \\"lastName\\", + \\"profile\\" + ] } }, \\"required\\": [ - \\"newsletter\\" + \\"data\\" ] - }, - \\"Email\\": { - \\"type\\": \\"string\\" - }, - \\"Address\\": { - \\"type\\": \\"string\\" } }, \\"securityDefinitions\\": { @@ -373,7 +373,9 @@ paths: headers: {} description: '' definitions: - UserBody: + Address: + type: string + CreateUserRequestBody: type: object properties: data: @@ -383,14 +385,22 @@ definitions: type: string lastName: type: string - profile: - $ref: '#/definitions/Profile' + age: + type: number + email: + $ref: '#/definitions/Email' + address: + $ref: '#/definitions/Address' required: - firstName - lastName - - profile + - age + - email + - address required: - data + Email: + type: string ErrorBody: type: object properties: @@ -403,30 +413,13 @@ definitions: required: - name - message - CreateUserRequestBody: + MessageOptions: type: object properties: - data: - type: object - properties: - firstName: - type: string - lastName: - type: string - age: - type: number - email: - $ref: '#/definitions/Email' - address: - $ref: '#/definitions/Address' - required: - - firstName - - lastName - - age - - email - - address + newsletter: + type: boolean required: - - data + - newsletter Profile: type: object properties: @@ -437,17 +430,24 @@ definitions: required: - private - messageOptions - MessageOptions: + UserBody: type: object properties: - newsletter: - type: boolean + data: + type: object + properties: + firstName: + type: string + lastName: + type: string + profile: + $ref: '#/definitions/Profile' + required: + - firstName + - lastName + - profile required: - - newsletter - Email: - type: string - Address: - type: string + - data securityDefinitions: securityHeader: type: apiKey diff --git a/lib/src/generators/contract/__snapshots__/openapi3.spec.ts.snap b/lib/src/generators/contract/__snapshots__/openapi3.spec.ts.snap index 6d9c70567..fb3096e62 100644 --- a/lib/src/generators/contract/__snapshots__/openapi3.spec.ts.snap +++ b/lib/src/generators/contract/__snapshots__/openapi3.spec.ts.snap @@ -192,7 +192,10 @@ exports[`OpenAPI 3 generator produces valid code: json 1`] = ` }, \\"components\\": { \\"schemas\\": { - \\"UserBody\\": { + \\"Address\\": { + \\"type\\": \\"string\\" + }, + \\"CreateUserRequestBody\\": { \\"type\\": \\"object\\", \\"properties\\": { \\"data\\": { @@ -204,14 +207,22 @@ exports[`OpenAPI 3 generator produces valid code: json 1`] = ` \\"lastName\\": { \\"type\\": \\"string\\" }, - \\"profile\\": { - \\"$ref\\": \\"#/components/schemas/Profile\\" + \\"age\\": { + \\"type\\": \\"number\\" + }, + \\"email\\": { + \\"$ref\\": \\"#/components/schemas/Email\\" + }, + \\"address\\": { + \\"$ref\\": \\"#/components/schemas/Address\\" } }, \\"required\\": [ \\"firstName\\", \\"lastName\\", - \\"profile\\" + \\"age\\", + \\"email\\", + \\"address\\" ] } }, @@ -219,6 +230,9 @@ exports[`OpenAPI 3 generator produces valid code: json 1`] = ` \\"data\\" ] }, + \\"Email\\": { + \\"type\\": \\"string\\" + }, \\"ErrorBody\\": { \\"type\\": \\"object\\", \\"properties\\": { @@ -237,39 +251,15 @@ exports[`OpenAPI 3 generator produces valid code: json 1`] = ` \\"message\\" ] }, - \\"CreateUserRequestBody\\": { + \\"MessageOptions\\": { \\"type\\": \\"object\\", \\"properties\\": { - \\"data\\": { - \\"type\\": \\"object\\", - \\"properties\\": { - \\"firstName\\": { - \\"type\\": \\"string\\" - }, - \\"lastName\\": { - \\"type\\": \\"string\\" - }, - \\"age\\": { - \\"type\\": \\"number\\" - }, - \\"email\\": { - \\"$ref\\": \\"#/components/schemas/Email\\" - }, - \\"address\\": { - \\"$ref\\": \\"#/components/schemas/Address\\" - } - }, - \\"required\\": [ - \\"firstName\\", - \\"lastName\\", - \\"age\\", - \\"email\\", - \\"address\\" - ] + \\"newsletter\\": { + \\"type\\": \\"boolean\\" } }, \\"required\\": [ - \\"data\\" + \\"newsletter\\" ] }, \\"Profile\\": { @@ -287,22 +277,32 @@ exports[`OpenAPI 3 generator produces valid code: json 1`] = ` \\"messageOptions\\" ] }, - \\"MessageOptions\\": { + \\"UserBody\\": { \\"type\\": \\"object\\", \\"properties\\": { - \\"newsletter\\": { - \\"type\\": \\"boolean\\" + \\"data\\": { + \\"type\\": \\"object\\", + \\"properties\\": { + \\"firstName\\": { + \\"type\\": \\"string\\" + }, + \\"lastName\\": { + \\"type\\": \\"string\\" + }, + \\"profile\\": { + \\"$ref\\": \\"#/components/schemas/Profile\\" + } + }, + \\"required\\": [ + \\"firstName\\", + \\"lastName\\", + \\"profile\\" + ] } }, \\"required\\": [ - \\"newsletter\\" + \\"data\\" ] - }, - \\"Email\\": { - \\"type\\": \\"string\\" - }, - \\"Address\\": { - \\"type\\": \\"string\\" } }, \\"securitySchemes\\": { @@ -440,7 +440,9 @@ paths: description: '' components: schemas: - UserBody: + Address: + type: string + CreateUserRequestBody: type: object properties: data: @@ -450,14 +452,22 @@ components: type: string lastName: type: string - profile: - $ref: '#/components/schemas/Profile' + age: + type: number + email: + $ref: '#/components/schemas/Email' + address: + $ref: '#/components/schemas/Address' required: - firstName - lastName - - profile + - age + - email + - address required: - data + Email: + type: string ErrorBody: type: object properties: @@ -470,30 +480,13 @@ components: required: - name - message - CreateUserRequestBody: + MessageOptions: type: object properties: - data: - type: object - properties: - firstName: - type: string - lastName: - type: string - age: - type: number - email: - $ref: '#/components/schemas/Email' - address: - $ref: '#/components/schemas/Address' - required: - - firstName - - lastName - - age - - email - - address + newsletter: + type: boolean required: - - data + - newsletter Profile: type: object properties: @@ -504,17 +497,24 @@ components: required: - private - messageOptions - MessageOptions: + UserBody: type: object properties: - newsletter: - type: boolean + data: + type: object + properties: + firstName: + type: string + lastName: + type: string + profile: + $ref: '#/components/schemas/Profile' + required: + - firstName + - lastName + - profile required: - - newsletter - Email: - type: string - Address: - type: string + - data securitySchemes: securityHeader: type: apiKey diff --git a/lib/src/generators/contract/openapi2.ts b/lib/src/generators/contract/openapi2.ts index eba8450d3..6a105b36e 100644 --- a/lib/src/generators/contract/openapi2.ts +++ b/lib/src/generators/contract/openapi2.ts @@ -70,12 +70,22 @@ export function openApiV2(contractDefinition: ContractDefinition): OpenApiV2 { }; return acc; }, {}), - definitions: contractDefinition.types.reduce<{ - [typeName: string]: OpenAPI2SchemaType; - }>((acc, typeNode) => { - acc[typeNode.name] = openApi2TypeSchema(typeNode.type); - return acc; - }, {}), + definitions: contractDefinition.types + .sort((prevType, currType) => { + if (prevType.name < currType.name) { + return -1; + } + if (prevType.name > currType.name) { + return 1; + } + return 0; + }) + .reduce<{ + [typeName: string]: OpenAPI2SchemaType; + }>((acc, typeNode) => { + acc[typeNode.name] = openApi2TypeSchema(typeNode.type); + return acc; + }, {}), ...(contractDefinition.api.securityHeader ? { securityDefinitions: { diff --git a/lib/src/generators/contract/openapi3.ts b/lib/src/generators/contract/openapi3.ts index e7e1e2694..ba7a6f22c 100644 --- a/lib/src/generators/contract/openapi3.ts +++ b/lib/src/generators/contract/openapi3.ts @@ -99,15 +99,25 @@ export function openApiV3(contractDefinition: ContractDefinition): OpenApiV3 { return acc; }, {}), components: { - schemas: contractDefinition.types.reduce<{ - [typeName: string]: OpenAPI3SchemaType; - }>((acc, typeNode) => { - acc[typeNode.name] = openApi3TypeSchema( - contractDefinition.types, - typeNode.type - ); - return acc; - }, {}), + schemas: contractDefinition.types + .sort((prevType, currType) => { + if (prevType.name < currType.name) { + return -1; + } + if (prevType.name > currType.name) { + return 1; + } + return 0; + }) + .reduce<{ + [typeName: string]: OpenAPI3SchemaType; + }>((acc, typeNode) => { + acc[typeNode.name] = openApi3TypeSchema( + contractDefinition.types, + typeNode.type + ); + return acc; + }, {}), securitySchemes: contractDefinition.api.securityHeader ? { [SECURITY_HEADER_SCHEME_NAME]: { diff --git a/lib/src/parsers/parser.spec.ts b/lib/src/parsers/parser.spec.ts index 750be3edb..77bde7c5c 100644 --- a/lib/src/parsers/parser.spec.ts +++ b/lib/src/parsers/parser.spec.ts @@ -3,7 +3,7 @@ import { parse } from "./parser"; describe("parser", () => { test("parses all information", () => { const result = parse("./lib/src/test/examples/contract.ts"); - expect(result.api).not.toBeUndefined; + expect(result.api).not.toBeUndefined(); expect(result.endpoints).toHaveLength(2); expect(result.types).toHaveLength(7); }); @@ -12,7 +12,7 @@ describe("parser", () => { const result = parse( "./lib/src/test/examples/recursive-imports-and-exports/contract.ts" ); - expect(result.api).not.toBeUndefined; + expect(result.api).not.toBeUndefined(); expect(result.endpoints).toHaveLength(4); }); }); diff --git a/lib/src/parsers/parser.ts b/lib/src/parsers/parser.ts index d683ec132..e342e5b0c 100644 --- a/lib/src/parsers/parser.ts +++ b/lib/src/parsers/parser.ts @@ -2,7 +2,6 @@ import path from "path"; import { CompilerOptions, Project, SourceFile, ts } from "ts-morph"; import { Locatable } from "../models/locatable"; import { ContractNode, EndpointNode } from "../models/nodes"; -import { ReferenceType } from "../models/types"; import { parseApi } from "./nodes/api-parser"; import { parseEndpoint } from "./nodes/endpoint-parser"; import { extractJsDocComment } from "./utilities/parser-utility"; @@ -12,7 +11,6 @@ import { } from "./utilities/type-parser"; import { retrieveTypeReferencesFromEndpoints, - retrieveTypeReferencesFromType, uniqueReferences } from "./utilities/type-reference-resolver"; @@ -72,29 +70,12 @@ function parseRootSourceFile( const endpoints = parseRecursively(file); - // Direct reference types, filtered to unique references for efficiency - const uniqueDirectReferenceTypes = uniqueReferences( - retrieveTypeReferencesFromEndpoints(endpoints) - ); - - // Reference types from the direct reference type hierarchy - const secondaryReferenceTypes = uniqueDirectReferenceTypes.reduce< - ReferenceType[] - >( - (referenceTypesAcc, currentReferenceType) => - referenceTypesAcc.concat( - retrieveTypeReferencesFromType(currentReferenceType, projectContext) - ), - [] - ); - - // Combine and filter to unique type references - const allReferenceTypes = uniqueReferences( - uniqueDirectReferenceTypes.concat(secondaryReferenceTypes) + const referenceTypes = uniqueReferences( + retrieveTypeReferencesFromEndpoints(endpoints, projectContext) ); // Construct the equivalent TypeNode for each reference type - const types = allReferenceTypes.map(referenceType => { + const types = referenceTypes.map(referenceType => { const file = projectContext.getSourceFileOrThrow(referenceType.location); const typeAlias = file.getTypeAlias(referenceType.name); diff --git a/lib/src/parsers/utilities/type-reference-resolver.ts b/lib/src/parsers/utilities/type-reference-resolver.ts index facf804fc..eda0ccbdf 100644 --- a/lib/src/parsers/utilities/type-reference-resolver.ts +++ b/lib/src/parsers/utilities/type-reference-resolver.ts @@ -22,104 +22,27 @@ import { import { parseInterfaceDeclaration, parseTypeNode } from "./type-parser"; /** - * Recursively retrieves all type references from a data type including itself. - */ -export function retrieveTypeReferencesFromType( - dataType: DataType, - projectContext: Project -): ReferenceType[] { - if (isReferenceType(dataType)) { - const file = projectContext.getSourceFileOrThrow(dataType.location); - switch (dataType.referenceKind) { - case TypeKind.NULL: - case TypeKind.BOOLEAN: - case TypeKind.STRING: - case TypeKind.NUMBER: - case TypeKind.INTEGER: - case TypeKind.DATE: - case TypeKind.DATE_TIME: - case TypeKind.BOOLEAN_LITERAL: - case TypeKind.STRING_LITERAL: - case TypeKind.NUMBER_LITERAL: - return [dataType]; - case TypeKind.ARRAY: - case TypeKind.UNION: - case TypeKind.TYPE_REFERENCE: { - const typeAlias = file.getTypeAliasOrThrow(dataType.name); - return [dataType].concat( - retrieveTypeReferencesFromType( - parseTypeNode(typeAlias.getTypeNodeOrThrow()), - projectContext - ) - ); - } - case TypeKind.OBJECT: { - const typeAlias = file.getTypeAlias(dataType.name); - if (typeAlias !== undefined) { - return [dataType].concat( - retrieveTypeReferencesFromType( - parseTypeNode(typeAlias.getTypeNodeOrThrow()), - projectContext - ) - ); - } - const interfaceDeclaration = file.getInterface(dataType.name); - if (interfaceDeclaration) { - return [dataType].concat( - retrieveTypeReferencesFromType( - parseInterfaceDeclaration(interfaceDeclaration), - projectContext - ) - ); - } - throw new Error( - "expected type reference to resolve to a type alias or an interface" - ); - } - default: { - throw new Error("unexpected type reference"); - } - } - } else if (isObjectType(dataType)) { - return dataType.properties.reduce( - (referenceTypesAcc, currentProperty) => - referenceTypesAcc.concat( - retrieveTypeReferencesFromType(currentProperty.type, projectContext) - ), - [] - ); - } else if (isArrayType(dataType)) { - return retrieveTypeReferencesFromType(dataType.elements, projectContext); - } else if (isUnionType(dataType)) { - return dataType.types.reduce( - (referenceTypesAcc, type) => - referenceTypesAcc.concat( - retrieveTypeReferencesFromType(type, projectContext) - ), - [] - ); - } else { - return []; - } -} - -/** - * Retrieve all the type references from a collection of endpoints. This will only retrieve direct references (not recursive) - * from each node of an endpoint. + * Retrieve all the type references from a collection of endpoints recurisvely. * * @param endpoints collection of endpoints + * @param projectContext ts-morph project */ export function retrieveTypeReferencesFromEndpoints( - endpoints: Array> + endpoints: Array>, + projectContext: Project ): ReferenceType[] { return endpoints.reduce( (referenceTypesAcc, currentEndpoint) => { const fromResponses = retrieveTypeReferencesFromResponses( - currentEndpoint.value.responses + currentEndpoint.value.responses, + projectContext ); if (currentEndpoint.value.request) { const fromRequest = fromResponses.concat( - retrieveTypeReferencesFromRequest(currentEndpoint.value.request) + retrieveTypeReferencesFromRequest( + currentEndpoint.value.request, + projectContext + ) ); return referenceTypesAcc.concat(fromRequest).concat(fromResponses); } @@ -130,30 +53,35 @@ export function retrieveTypeReferencesFromEndpoints( } /** - * Retrieve all type references from a request. This will only retrieve direct references (not recursive). + * Retrieve all type references from a request recursively. * * @param request a request + * @param projectContext ts-morph project */ function retrieveTypeReferencesFromRequest( - request: Locatable + request: Locatable, + projectContext: Project ): ReferenceType[] { const fromHeaders = request.value.headers ? retrieveTypeReferencesFromHeadersPathParamsQueryParams( - request.value.headers.value + request.value.headers.value, + projectContext ) : []; const fromPathParams = request.value.pathParams ? retrieveTypeReferencesFromHeadersPathParamsQueryParams( - request.value.pathParams.value + request.value.pathParams.value, + projectContext ) : []; const fromQueryParams = request.value.queryParams ? retrieveTypeReferencesFromHeadersPathParamsQueryParams( - request.value.queryParams.value + request.value.queryParams.value, + projectContext ) : []; const fromBody = request.value.body - ? retrieveTypeReferencesFromBody(request.value.body) + ? retrieveTypeReferencesFromBody(request.value.body, projectContext) : []; return fromHeaders @@ -163,22 +91,28 @@ function retrieveTypeReferencesFromRequest( } /** - * Retrieve all type references from a collection of responses. This will only retrieve direct references (not recursive). + * Retrieve all type references from a collection of responses recursively. * * @param requests a collection of responses + * @param projectContext ts-morph project */ function retrieveTypeReferencesFromResponses( - responses: Array> + responses: Array>, + projectContext: Project ): ReferenceType[] { return responses.reduce( (typeReferencesAcc, currentResponse) => { const fromHeaders = currentResponse.value.headers ? retrieveTypeReferencesFromHeadersPathParamsQueryParams( - currentResponse.value.headers.value + currentResponse.value.headers.value, + projectContext ) : []; const fromBody = currentResponse.value.body - ? retrieveTypeReferencesFromBody(currentResponse.value.body) + ? retrieveTypeReferencesFromBody( + currentResponse.value.body, + projectContext + ) : []; return typeReferencesAcc.concat(fromHeaders).concat(fromBody); }, @@ -187,52 +121,117 @@ function retrieveTypeReferencesFromResponses( } /** - * Retrieve all type references from a body. This will only retrieve direct references (not recursive). + * Retrieve all type references from a body recursively. * * @param body a body + * @param projectContext ts-morph project */ function retrieveTypeReferencesFromBody( - body: Locatable + body: Locatable, + projectContext: Project ): ReferenceType[] { - const type = body.value.type; - if (isReferenceType(type)) { - return [type]; - } else if (isUnionType(type)) { - return type.types.reduce( - (typeAcc, currType) => - isReferenceType(currType) ? typeAcc.concat(currType) : typeAcc, - [] - ); - } else { - return []; - } + return retrieveTypeReferencesFromType(body.value.type, projectContext); } /** - * Retrieve all type references from a collection of headers, path params or query params. - * This will only retrieve direct references (not recursive). + * Retrieve all type references from a collection of headers, path params or query params recursively. * * @param nodes a collection of headers, path params or query params + * @param projectContext ts-morph project */ function retrieveTypeReferencesFromHeadersPathParamsQueryParams( - nodes: Array> + nodes: Array>, + projectContext: Project ): ReferenceType[] { - return nodes.reduce((typeReferencesAcc, currentHeader) => { - const type = currentHeader.value.type; - if (isReferenceType(type)) { - return typeReferencesAcc.concat(type); - } else if (isUnionType(type)) { - return typeReferencesAcc.concat( - type.types.reduce( - (typeAcc, currType) => - isReferenceType(currType) ? typeAcc.concat(currType) : typeAcc, - [] - ) - ); - } else { - return typeReferencesAcc; + return nodes.reduce( + (typeReferencesAcc, currentHeader) => + typeReferencesAcc.concat( + retrieveTypeReferencesFromType(currentHeader.value.type, projectContext) + ), + [] + ); +} + +/** + * Recursively retrieves all type references from a data type including itself. + */ +function retrieveTypeReferencesFromType( + dataType: DataType, + projectContext: Project +): ReferenceType[] { + if (isReferenceType(dataType)) { + const file = projectContext.getSourceFileOrThrow(dataType.location); + switch (dataType.referenceKind) { + case TypeKind.NULL: + case TypeKind.BOOLEAN: + case TypeKind.STRING: + case TypeKind.NUMBER: + case TypeKind.INTEGER: + case TypeKind.DATE: + case TypeKind.DATE_TIME: + case TypeKind.BOOLEAN_LITERAL: + case TypeKind.STRING_LITERAL: + case TypeKind.NUMBER_LITERAL: + return [dataType]; + case TypeKind.ARRAY: + case TypeKind.UNION: + case TypeKind.TYPE_REFERENCE: { + const typeAlias = file.getTypeAliasOrThrow(dataType.name); + return [dataType].concat( + retrieveTypeReferencesFromType( + parseTypeNode(typeAlias.getTypeNodeOrThrow()), + projectContext + ) + ); + } + case TypeKind.OBJECT: { + const typeAlias = file.getTypeAlias(dataType.name); + if (typeAlias !== undefined) { + return [dataType].concat( + retrieveTypeReferencesFromType( + parseTypeNode(typeAlias.getTypeNodeOrThrow()), + projectContext + ) + ); + } + const interfaceDeclaration = file.getInterface(dataType.name); + if (interfaceDeclaration) { + return [dataType].concat( + retrieveTypeReferencesFromType( + parseInterfaceDeclaration(interfaceDeclaration), + projectContext + ) + ); + } + throw new Error( + "expected type reference to resolve to a type alias or an interface" + ); + } + default: { + throw new Error("unexpected type reference"); + } } - }, []); + } else if (isObjectType(dataType)) { + return dataType.properties.reduce( + (referenceTypesAcc, currentProperty) => + referenceTypesAcc.concat( + retrieveTypeReferencesFromType(currentProperty.type, projectContext) + ), + [] + ); + } else if (isArrayType(dataType)) { + return retrieveTypeReferencesFromType(dataType.elements, projectContext); + } else if (isUnionType(dataType)) { + return dataType.types.reduce( + (referenceTypesAcc, type) => + referenceTypesAcc.concat( + retrieveTypeReferencesFromType(type, projectContext) + ), + [] + ); + } else { + return []; + } } /**