chore(oas): [19/n] improve oas schemas (#9317)

- Improve OAS schemas [19/n]
- docs-generator: combine `Date | string` type to just `Date`.
This commit is contained in:
Shahed Nasser
2024-09-26 16:44:57 +03:00
committed by GitHub
parent 5390c8a845
commit af34bf6c0c
18 changed files with 2472 additions and 171 deletions

View File

@@ -0,0 +1,83 @@
import ts from "typescript"
type ConstructorParams = {
checker: ts.TypeChecker
}
export default class TypesHelper {
private checker: ts.TypeChecker
constructor({ checker }: ConstructorParams) {
this.checker = checker
}
areTypesEqual(type1: ts.Type, type2: ts.Type): boolean {
return "id" in type1 && "id" in type2 && type1.id === type2.id
}
/**
* Retrieve the name of a type. This is useful when retrieving allowed/disallowed
* properties in an Omit/Pick type.
*
* @param itemType - The type to retrieve its name.
* @returns The type's name.
*/
getTypeName(itemType: ts.Type): string {
if (itemType.symbol || itemType.aliasSymbol) {
return (itemType.aliasSymbol || itemType.symbol).name
}
if (itemType.isLiteral()) {
return itemType.value.toString()
}
return this.checker.typeToString(itemType)
}
cleanUpTypes(types: ts.Type[]): ts.Type[] {
let cleanedUpTypes = this.removeStringRegExpTypeOverlaps(types)
cleanedUpTypes = this.joinDateAndString(cleanedUpTypes)
return cleanedUpTypes
}
private removeStringRegExpTypeOverlaps(types: ts.Type[]): ts.Type[] {
return types.filter((itemType) => {
// remove overlapping string / regexp types
if (this.checker.typeToString(itemType) === "RegExp") {
const hasString = types.some((t) => {
return (
t.flags === ts.TypeFlags.String ||
t.flags === ts.TypeFlags.StringLiteral
)
})
return !hasString
}
return true
})
}
private joinDateAndString(types: ts.Type[]): ts.Type[] {
if (types.length !== 2) {
return types
}
let dateType: ts.Type | undefined
let hasStringType = false
types.forEach((tsType) => {
if (
tsType.flags === ts.TypeFlags.String ||
tsType.flags === ts.TypeFlags.StringLiteral
) {
hasStringType = true
} else if (this.getTypeName(tsType) === "Date") {
dateType = tsType
}
})
return dateType && hasStringType ? [dateType] : types
}
}

View File

@@ -26,6 +26,7 @@ import FunctionKindGenerator, {
VariableNode,
} from "./function.js"
import { API_ROUTE_PARAM_REGEX } from "../../constants.js"
import TypesHelper from "../helpers/types.js"
import {
isLevelExceeded,
maybeIncrementLevel,
@@ -93,6 +94,7 @@ class OasKindGenerator extends FunctionKindGenerator {
protected oasExamplesGenerator: OasExamplesGenerator
protected oasSchemaHelper: OasSchemaHelper
protected schemaFactory: SchemaFactory
protected typesHelper: TypesHelper
constructor(options: GeneratorOptions) {
super(options)
@@ -103,6 +105,9 @@ class OasKindGenerator extends FunctionKindGenerator {
this.tags = new Map()
this.oasSchemaHelper = new OasSchemaHelper()
this.schemaFactory = new SchemaFactory()
this.typesHelper = new TypesHelper({
checker: this.checker,
})
this.init()
this.generatorEventManager.listen(
@@ -1397,18 +1402,18 @@ class OasKindGenerator extends FunctionKindGenerator {
}
}
const oneOfItems = this.removeStringRegExpTypeOverlaps(
(itemType as ts.UnionType).types
).map((unionType) =>
this.typeToSchema({
itemType: unionType,
level: maybeIncrementLevel(level, "oneOf"),
title,
descriptionOptions,
saveSchema,
...rest,
})
)
const oneOfItems = this.typesHelper
.cleanUpTypes((itemType as ts.UnionType).types)
.map((unionType) =>
this.typeToSchema({
itemType: unionType,
level: maybeIncrementLevel(level, "oneOf"),
title,
descriptionOptions,
saveSchema,
...rest,
})
)
if (oneOfItems.length === 1) {
return oneOfItems[0]
@@ -1418,18 +1423,18 @@ class OasKindGenerator extends FunctionKindGenerator {
oneOf: oneOfItems,
}
case itemType.isIntersection():
const allOfItems = this.removeStringRegExpTypeOverlaps(
(itemType as ts.IntersectionType).types
).map((intersectionType) => {
return this.typeToSchema({
itemType: intersectionType,
level: maybeIncrementLevel(level, "allOf"),
title,
descriptionOptions,
saveSchema,
...rest,
const allOfItems = this.typesHelper
.cleanUpTypes((itemType as ts.IntersectionType).types)
.map((intersectionType) => {
return this.typeToSchema({
itemType: intersectionType,
level: maybeIncrementLevel(level, "allOf"),
title,
descriptionOptions,
saveSchema,
...rest,
})
})
})
if (allOfItems.length === 1) {
return allOfItems[0]
@@ -1448,9 +1453,9 @@ class OasKindGenerator extends FunctionKindGenerator {
}
const pickedProperties = pickTypeArgs[1].isUnion()
? pickTypeArgs[1].types.map((unionType) =>
this.getTypeName(unionType)
this.typesHelper.getTypeName(unionType)
)
: [this.getTypeName(pickTypeArgs[1])]
: [this.typesHelper.getTypeName(pickTypeArgs[1])]
return this.typeToSchema({
itemType: pickTypeArgs[0],
title,
@@ -1470,9 +1475,9 @@ class OasKindGenerator extends FunctionKindGenerator {
}
const omitProperties = omitTypeArgs[1].isUnion()
? omitTypeArgs[1].types.map((unionType) =>
this.getTypeName(unionType)
this.typesHelper.getTypeName(unionType)
)
: [this.getTypeName(omitTypeArgs[1])]
: [this.typesHelper.getTypeName(omitTypeArgs[1])]
return this.typeToSchema({
itemType: omitTypeArgs[0],
title,
@@ -1581,7 +1586,7 @@ class OasKindGenerator extends FunctionKindGenerator {
const arrHasParentType =
rest.context !== "query" &&
this.checker.isArrayType(propertyType) &&
this.areTypesEqual(
this.typesHelper.areTypesEqual(
itemType,
this.checker.getTypeArguments(
propertyType as ts.TypeReference
@@ -1589,7 +1594,7 @@ class OasKindGenerator extends FunctionKindGenerator {
)
const isParentType =
rest.context !== "query" &&
this.areTypesEqual(itemType, propertyType)
this.typesHelper.areTypesEqual(itemType, propertyType)
if (isParentType && objSchema["x-schemaName"]) {
properties[property.name] = {
@@ -1792,25 +1797,6 @@ class OasKindGenerator extends FunctionKindGenerator {
}
}
/**
* Retrieve the name of a type. This is useful when retrieving allowed/disallowed
* properties in an Omit/Pick type.
*
* @param itemType - The type to retrieve its name.
* @returns The type's name.
*/
getTypeName(itemType: ts.Type): string {
if (itemType.symbol || itemType.aliasSymbol) {
return (itemType.aliasSymbol || itemType.symbol).name
}
if (itemType.isLiteral()) {
return itemType.value.toString()
}
return this.checker.typeToString(itemType)
}
/**
* Initialize the {@link tags} property.
*/
@@ -2299,27 +2285,6 @@ class OasKindGenerator extends FunctionKindGenerator {
fnText.includes(responseType)
)
}
private removeStringRegExpTypeOverlaps(types: ts.Type[]): ts.Type[] {
return types.filter((itemType) => {
// remove overlapping string / regexp types
if (this.checker.typeToString(itemType) === "RegExp") {
const hasString = types.some((t) => {
return (
t.flags === ts.TypeFlags.String ||
t.flags === ts.TypeFlags.StringLiteral
)
})
return !hasString
}
return true
})
}
private areTypesEqual(type1: ts.Type, type2: ts.Type): boolean {
return "id" in type1 && "id" in type2 && type1.id === type2.id
}
}
export default OasKindGenerator