Skip to content

Commit

Permalink
Merge pull request #532 from hey-api/fix/incorrect-value-escapes
Browse files Browse the repository at this point in the history
fix: broken encoding
  • Loading branch information
mrlubos committed May 5, 2024
2 parents 8a20551 + a9c764e commit f57123b
Show file tree
Hide file tree
Showing 18 changed files with 181 additions and 138 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-socks-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

fix: broken encoding
63 changes: 7 additions & 56 deletions packages/openapi-ts/src/compiler/classes.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,15 @@
import ts from 'typescript';

import { createTypeNode } from './typedef';
import { toExpression } from './types';
import {
type AccessLevel,
type FunctionParameter,
toAccessLevelModifiers,
toExpression,
toParameterDeclarations,
} from './types';
import { addLeadingJSDocComment, Comments, isType } from './utils';

type AccessLevel = 'public' | 'protected' | 'private';

export type FunctionParameter = {
accessLevel?: AccessLevel;
default?: any;
isReadOnly?: boolean;
isRequired?: boolean;
name: string;
type: any | ts.TypeNode;
};

/**
* Convert AccessLevel to proper TypeScript compiler API modifier.
* @param access - the access level.
* @returns ts.ModifierLike[]
*/
const toAccessLevelModifiers = (access?: AccessLevel): ts.ModifierLike[] => {
const keyword =
access === 'public'
? ts.SyntaxKind.PublicKeyword
: access === 'protected'
? ts.SyntaxKind.ProtectedKeyword
: access === 'private'
? ts.SyntaxKind.PrivateKeyword
: undefined;
const modifiers: ts.ModifierLike[] = [];
if (keyword) {
modifiers.push(ts.factory.createModifier(keyword));
}
return modifiers;
};

/**
* Convert parameters to the declaration array expected by compiler API.
* @param parameters - the parameters to conver to declarations
* @returns ts.ParameterDeclaration[]
*/
export const toParameterDeclarations = (parameters: FunctionParameter[]) =>
parameters.map((p) => {
const modifiers = toAccessLevelModifiers(p.accessLevel);
if (p.isReadOnly) {
modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
}
return ts.factory.createParameterDeclaration(
modifiers,
undefined,
ts.factory.createIdentifier(p.name),
p.isRequired !== undefined && !p.isRequired
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
p.type !== undefined ? createTypeNode(p.type) : undefined,
p.default !== undefined ? toExpression({ value: p.default }) : undefined,
);
});

/**
* Create a class constructor declaration.
* @param accessLevel - the access level of the constructor.
Expand Down
14 changes: 8 additions & 6 deletions packages/openapi-ts/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import * as typedef from './typedef';
import * as types from './types';
import { stringToTsNodes, tsNodeToString } from './utils';

export type { FunctionParameter } from './classes';
export type { Property } from './typedef';
export type { FunctionParameter } from './types';
export type { Comments } from './utils';
export type { ClassElement, Node, TypeNode } from 'typescript';

Expand Down Expand Up @@ -52,11 +52,11 @@ export class TypeScriptFile {
}
}

public add(...nodes: Array<ts.Node | string>): void {
public add(...nodes: Array<ts.Node | string>) {
this._items = [...this._items, ...nodes];
}

public addImport(...params: Parameters<typeof compiler.import.named>): void {
public addImport(...params: Parameters<typeof compiler.import.named>) {
this._imports = [...this._imports, compiler.import.named(...params)];
}

Expand Down Expand Up @@ -94,13 +94,15 @@ export class TypeScriptFile {
if (this._imports.length) {
output = [
...output,
this._imports.map((v) => tsNodeToString(v)).join('\n'),
this._imports.map((node) => tsNodeToString({ node })).join('\n'),
];
}
output = [
...output,
...this._items.map((v) =>
typeof v === 'string' ? v : tsNodeToString(v),
...this._items.map((node) =>
typeof node === 'string'
? node
: tsNodeToString({ node, unescape: true }),
),
];
return output.join(seperator);
Expand Down
57 changes: 31 additions & 26 deletions packages/openapi-ts/src/compiler/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import ts from 'typescript';

import { addLeadingJSDocComment, type Comments, ots } from './utils';
import {
addLeadingJSDocComment,
type Comments,
type ImportItemObject,
ots,
} from './utils';

/**
* Create export all declaration. Example: `export * from './y'`.
Expand All @@ -15,9 +20,7 @@ export const createExportAllDeclaration = (module: string) =>
ots.string(module),
);

type ImportItem =
| { name: string; isTypeOnly?: boolean; alias?: string }
| string;
type ImportItem = ImportItemObject | string;

/**
* Create a named export declaration. Example: `export { X } from './y'`.
Expand Down Expand Up @@ -103,28 +106,30 @@ export const createNamedImportDeclarations = (
items: Array<ImportItem> | ImportItem,
module: string,
): ts.ImportDeclaration => {
items = Array.isArray(items) ? items : [items];
const isAllTypes = items.every((i) => typeof i === 'object' && i.isTypeOnly);
return ts.factory.createImportDeclaration(
const importedTypes = Array.isArray(items) ? items : [items];
const isTypeOnly = !importedTypes.some(
(item) => typeof item !== 'object' || !item.isTypeOnly,
);
const elements = importedTypes.map((item) => {
const importedType: ImportItemObject =
typeof item === 'string' ? { name: item } : item;
return ots.import({
alias: importedType.alias,
isTypeOnly: isTypeOnly ? false : Boolean(importedType.isTypeOnly),
name: importedType.name,
});
});
const namedBindings = ts.factory.createNamedImports(elements);
const importClause = ts.factory.createImportClause(
isTypeOnly,
undefined,
ts.factory.createImportClause(
isAllTypes,
undefined,
ts.factory.createNamedImports(
items.map((item) => {
const {
name,
isTypeOnly = undefined,
alias = undefined,
} = typeof item === 'string' ? { name: item } : item;
return ots.import(
name,
isAllTypes ? false : Boolean(isTypeOnly),
alias,
);
}),
),
),
ots.string(module),
namedBindings,
);
const moduleSpecifier = ots.string(module);
const statement = ts.factory.createImportDeclaration(
undefined,
importClause,
moduleSpecifier,
);
return statement;
};
2 changes: 1 addition & 1 deletion packages/openapi-ts/src/compiler/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const createTypeRecordNode = (
const node = createTypeInterfaceNode([
{
isRequired: true,
name: `[key: ${tsNodeToString(keyNode)}]`,
name: `[key: ${tsNodeToString({ node: keyNode, unescape: true })}]`,
type: valueNode,
},
]);
Expand Down
58 changes: 57 additions & 1 deletion packages/openapi-ts/src/compiler/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import ts from 'typescript';

import { type FunctionParameter, toParameterDeclarations } from './classes';
import { createTypeNode } from './typedef';
import { addLeadingJSDocComment, type Comments, isType, ots } from './utils';

export type AccessLevel = 'public' | 'protected' | 'private';

export type FunctionParameter = {
accessLevel?: AccessLevel;
default?: any;
isReadOnly?: boolean;
isRequired?: boolean;
name: string;
type: any | ts.TypeNode;
};

/**
* Convert an unknown value to an expression.
* @param identifiers - list of keys that are treated as identifiers.
Expand Down Expand Up @@ -48,6 +58,52 @@ export const toExpression = <T = unknown>({
}
};

/**
* Convert AccessLevel to proper TypeScript compiler API modifier.
* @param access - the access level.
* @returns ts.ModifierLike[]
*/
export const toAccessLevelModifiers = (
access?: AccessLevel,
): ts.ModifierLike[] => {
const keyword =
access === 'public'
? ts.SyntaxKind.PublicKeyword
: access === 'protected'
? ts.SyntaxKind.ProtectedKeyword
: access === 'private'
? ts.SyntaxKind.PrivateKeyword
: undefined;
const modifiers: ts.ModifierLike[] = [];
if (keyword) {
modifiers.push(ts.factory.createModifier(keyword));
}
return modifiers;
};

/**
* Convert parameters to the declaration array expected by compiler API.
* @param parameters - the parameters to conver to declarations
* @returns ts.ParameterDeclaration[]
*/
export const toParameterDeclarations = (parameters: FunctionParameter[]) =>
parameters.map((p) => {
const modifiers = toAccessLevelModifiers(p.accessLevel);
if (p.isReadOnly) {
modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
}
return ts.factory.createParameterDeclaration(
modifiers,
undefined,
ts.factory.createIdentifier(p.name),
p.isRequired !== undefined && !p.isRequired
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
p.type !== undefined ? createTypeNode(p.type) : undefined,
p.default !== undefined ? toExpression({ value: p.default }) : undefined,
);
});

/**
* Create Function type expression.
*/
Expand Down

0 comments on commit f57123b

Please sign in to comment.