Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 1 addition & 24 deletions plugins/typescript/src/core/schemaToEnumDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
import { pascal } from "case";
import { SchemaObject } from "openapi3-ts/oas30";
import ts, { factory as f } from "typescript";
import { isValidIdentifier } from "tsutils";
import { convertNumberToWord } from "../utils/getEnumProperties";
import { Context, getJSDocComment } from "./schemaToTypeAliasDeclaration";

/**
* Function to check if a string is a valid TypeScript identifier
*
* @param name Name to check
*/
function isValidIdentifier(name: string): boolean {
if (name.length === 0) {
return false;
}

const firstChar = name.charCodeAt(0);
if (!ts.isIdentifierStart(firstChar, ts.ScriptTarget.Latest)) {
return false;
}

for (let i = 1; i < name.length; i++) {
if (!ts.isIdentifierPart(name.charCodeAt(i), ts.ScriptTarget.Latest)) {
return false;
}
}

return true;
}

/**
* Add Enum support when transforming an OpenAPI Schema Object to Typescript Nodes.
*
Expand Down
24 changes: 24 additions & 0 deletions plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,30 @@ describe("schemaToTypeAliasDeclaration", () => {
`);
});

it("should generate valid identifier from number name", () => {
const schema: SchemaObject = {};

expect(printSchema(schema, "200")).toMatchInlineSnapshot(`
"export type TwoHundred = void;"
`);
});

it("should generate valid identifier from symbol name", () => {
const schema: SchemaObject = {};

expect(printSchema(schema, "-")).toMatchInlineSnapshot(`
"export type _ = void;"
`);
});

it("should generate valid identifier from invalid name", () => {
const schema: SchemaObject = {};

expect(printSchema(schema, "🙂")).toMatchInlineSnapshot(`
"export type _ = void;"
`);
});

it("should generate a `never` if the combined type is broken", () => {
const schema: SchemaObject = {
allOf: [{ type: "string" }, { type: "number" }],
Expand Down
43 changes: 37 additions & 6 deletions plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
} from "openapi3-ts/oas30";
import { singular } from "pluralize";
import { isValidIdentifier } from "tsutils";
import ts, { factory as f } from "typescript";
import ts, {
factory as f,
isIdentifierPart,
isIdentifierStart,
} from "typescript";
import { convertNumberToWord } from "../utils/getEnumProperties";
import { getReferenceSchema } from "./getReference";

type RemoveIndex<T> = {
Expand Down Expand Up @@ -56,11 +61,37 @@ export const schemaToTypeAliasDeclaration = (
const jsDocNode = isSchemaObject(schema)
? getJSDocComment(schema, context)
: undefined;

let identifier = pascal(name);

if (identifier.length > 0 && !isNaN(Number(identifier))) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check will also convert "0123" to "OneTwoThree", as it can convert it to a number.

What do you prefer? Either we ignore this, add a handling for "Zero" at the start or we let the check fail and handle this case in the "else"-block

// If the identifier can be cast to a number, convert it to a word.
identifier = pascal(convertNumberToWord(Number(identifier)));
} else {
// If the identifier does not start with a valid character but valid identifier part, prefix it with an underscore.
if (
!isIdentifierStart(identifier.charCodeAt(0), ts.ScriptTarget.Latest) &&
isIdentifierPart(identifier.charCodeAt(0), ts.ScriptTarget.Latest)
) {
identifier = `_${identifier}`;
}

// If the identifier is still not valid, remove invalid characters.
if (!isValidIdentifier(identifier)) {
identifier = identifier.replace(/[^a-zA-Z0-9_]/g, "");
}

// If the identifier is now empty, set it to "_".
if (identifier.length === 0) {
identifier = "_";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could name it here "NoValidName" or we use the invalid name here.

I just kept the "_" as placeholder.

}
}

const declarationNode = f.createTypeAliasDeclaration(
[f.createModifier(ts.SyntaxKind.ExportKeyword)],
pascal(name),
identifier,
undefined,
getType(schema, context, name)
getType(schema, context, identifier)
);

return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode];
Expand Down Expand Up @@ -89,10 +120,10 @@ export const getType = (

let refNode: ts.TypeNode = f.createTypeReferenceNode(
namespace === context.currentComponent
? f.createIdentifier(pascal(name))
? f.createIdentifier(name)
: f.createQualifiedName(
f.createIdentifier(pascal(namespace)),
f.createIdentifier(pascal(name))
f.createIdentifier(name)
)
);

Expand Down Expand Up @@ -174,7 +205,7 @@ export const getType = (
if (schema.enum) {
if (isNodeEnum) {
return f.createUnionTypeNode([
f.createTypeReferenceNode(f.createIdentifier(pascal(name || ""))),
f.createTypeReferenceNode(f.createIdentifier(name || "")),
...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []),
]);
}
Expand Down