Skip to content

Commit

Permalink
feat: update codegen api and types (#17)
Browse files Browse the repository at this point in the history
BREAKING_CHANGE: rewrite api name
  • Loading branch information
Himenon committed Feb 1, 2021
1 parent 71569cc commit 1c6e748
Show file tree
Hide file tree
Showing 15 changed files with 80 additions and 30 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ yarn add -D @himenon/openapi-typescript-code-generator
### DEMO

- [DEMO](./example/README.md)
- [DEMO: github/rest-api-client code generate](https://github.com/Himenon/github-rest-api-client/tree/master/source)
- https://github.com/github/rest-api-description

### Basic usage

Expand All @@ -41,7 +43,7 @@ main();

### Create the original API Client template.

We have an entry point in `option.makeApiClient` to generate non-typed code.
We have an entry point in `option.rewriteCodeAfterTypeDeclaration` to generate non-typed code.
The first argument can be TypeScript's `TransformationContext`, and the second argument contains the information of the type definition generated before this.
By using [ts-ast-viewer](https://ts-ast-viewer.com), code extension by AST can facilitate code extension.

Expand All @@ -56,7 +58,7 @@ const main = () => {
const params: CodeGenerator.Params = {
entryPoint: "your/openapi/spec.yml", // support .yml, .yaml, .json
option: {
makeApiClient: (context: ts.TransformationContext, codeGeneratorParamsList: CodeGenerator.Converter.v3.CodeGeneratorParams[]): ts.Statement[] => {
rewriteCodeAfterTypeDeclaration: (context: ts.TransformationContext, codeGeneratorParamsList: CodeGenerator.Converter.v3.CodeGeneratorParams[]): ts.Statement[] => {
const factory = context.factory; // https://ts-ast-viewer.com/ is very very very useful !
return []; // generate no api client
},
Expand Down
4 changes: 2 additions & 2 deletions docs/ja/README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ main();

### オリジナルの API Client テンプレートを作成する

`option.makeApiClient`に型定義以外のコードを生成するためのエントリーポイントを用意しています。
`option.rewriteCodeAfterTypeDeclaration`に型定義以外のコードを生成するためのエントリーポイントを用意しています。
第 1 引数は TypeScript の`TransformationContext`が利用でき、第 2 引数はこれ以前に生成した型定義の情報が含まれます。
[ts-ast-viewer](https://ts-ast-viewer.com)を利用することにより AST によるコード拡張がコード拡張を円滑にでます。

Expand All @@ -54,7 +54,7 @@ const main = () => {
const params: CodeGenerator.Params = {
entryPoint: "your/openapi/spec.yml", // support .yml, .yaml, .json
option: {
makeApiClient: (context: ts.TransformationContext, codeGeneratorParamsList: CodeGenerator.Converter.v3.CodeGeneratorParams[]): ts.Statement[] => {
rewriteCodeAfterTypeDeclaration: (context: ts.TransformationContext, codeGeneratorParamsList: CodeGenerator.Converter.v3.CodeGeneratorParams[]): ts.Statement[] => {
const factory = context.factory; // https://ts-ast-viewer.com/ is very very very useful !
return []; // generate no api client
},
Expand Down
7 changes: 4 additions & 3 deletions example/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Generated by @himenon/openapi-typescript-code-generator v0.0.0
// Generated by @himenon/openapi-typescript-code-generator v0.2.0
//
// OpenApi : 3.0.3
//
Expand Down Expand Up @@ -68,15 +68,16 @@ export interface QueryParameter {
export interface QueryParameters {
[key: string]: QueryParameter;
}
export type SuccessResponses = Response$getBooks$Status$200 | Response$searchBooks$Status$200;
export interface ApiClient<RequestOption> {
request: (
request: <T = SuccessResponses>(
httpMethod: HttpMethod,
url: string,
headers: ObjectLike | any,
requestBody: ObjectLike | any,
queryParameters: QueryParameters | undefined,
options?: RequestOption,
) => Promise<any>;
) => Promise<T>;
}
export class Client<RequestOption> {
constructor(private apiClient: ApiClient<RequestOption>, private baseUrl: string) {}
Expand Down
3 changes: 2 additions & 1 deletion src/CodeGenerator/factory/CallExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ts from "typescript";

export interface Params$Create {
expression: ts.Expression;
typeArguments?: readonly ts.TypeNode[] | undefined;
argumentsArray: readonly ts.Expression[];
}

Expand All @@ -10,7 +11,7 @@ export interface Factory {
}

export const create = ({ factory }: ts.TransformationContext): Factory["create"] => (params: Params$Create): ts.CallExpression => {
const node = factory.createCallExpression(params.expression, undefined, params.argumentsArray);
const node = factory.createCallExpression(params.expression, params.typeArguments, params.argumentsArray);
return node;
};

Expand Down
3 changes: 2 additions & 1 deletion src/CodeGenerator/factory/TypeParameterDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import ts from "typescript";
export interface Params$Create {
name: string;
constraint?: ts.TypeNode;
defaultType?: ts.TypeNode;
}

export interface Factory {
create: (params: Params$Create) => ts.TypeParameterDeclaration;
}

export const create = ({ factory }: ts.TransformationContext): Factory["create"] => (params: Params$Create): ts.TypeParameterDeclaration => {
const node = factory.createTypeParameterDeclaration(factory.createIdentifier(params.name), params.constraint, undefined);
const node = factory.createTypeParameterDeclaration(factory.createIdentifier(params.name), params.constraint, params.defaultType);
return node;
};

Expand Down
9 changes: 6 additions & 3 deletions src/Converter/v3/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,17 @@ const generateCodeGeneratorParamsList = (store: Store.Type, converterContext: Co
return params;
};

export type MakeApiClientFunction = (context: ts.TransformationContext, codeGeneratorParamsList: CodeGeneratorParams[]) => ts.Statement[];
export type RewriteCodeAfterTypeDeclaration = (
context: ts.TransformationContext,
codeGeneratorParamsList: CodeGeneratorParams[],
) => ts.Statement[];

export const generateApiClientCode = (
store: Store.Type,
context: ts.TransformationContext,
converterContext: ConverterContext.Types,
makeApiClient: MakeApiClientFunction,
rewriteCodeAfterTypeDeclaration: RewriteCodeAfterTypeDeclaration,
): void => {
const codeGeneratorParamsList = generateCodeGeneratorParamsList(store, converterContext);
store.addAdditionalStatement(makeApiClient(context, codeGeneratorParamsList));
store.addAdditionalStatement(rewriteCodeAfterTypeDeclaration(context, codeGeneratorParamsList));
};
7 changes: 5 additions & 2 deletions src/Converter/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export interface Type {
}

export interface Option {
makeApiClient: Generator.MakeApiClientFunction;
/**
* It is possible to rewrite the implementation after the type declaration.
*/
rewriteCodeAfterTypeDeclaration: Generator.RewriteCodeAfterTypeDeclaration;
}

export const create = (entryPoint: string, rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document, option: Option): Type => {
Expand Down Expand Up @@ -94,7 +97,7 @@ export const create = (entryPoint: string, rootSchema: OpenApi.Document, noRefer
}
if (rootSchema.paths) {
Paths.generateStatements(entryPoint, currentPoint, store, factory, rootSchema.paths, toTypeNodeContext, converterContext);
Generator.generateApiClientCode(store, context, converterContext, option.makeApiClient);
Generator.generateApiClientCode(store, context, converterContext, option.rewriteCodeAfterTypeDeclaration);
}
return store.getRootStatements();
};
Expand Down
42 changes: 39 additions & 3 deletions src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import ts from "typescript";

import { Factory } from "../../CodeGenerator";
import { CodeGeneratorParams } from "../../Converter/v3";

const httpMethodList: string[] = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];

const createSuccessResponseTypeAlias = (typeName: string, factory: Factory.Type, successResponseNames: string[]) => {
if (successResponseNames.length === 0) {
return factory.TypeAliasDeclaration.create({
export: true,
name: typeName,
type: ts.factory.createToken(ts.SyntaxKind.VoidKeyword),
});
}
return factory.TypeAliasDeclaration.create({
export: true,
name: typeName,
type: factory.UnionTypeNode.create({
typeNodes: successResponseNames.map(name => {
return factory.TypeReferenceNode.create({
name,
});
}),
}),
});
};

const createHttpMethod = (factory: Factory.Type) => {
return factory.TypeAliasDeclaration.create({
export: true,
Expand Down Expand Up @@ -61,7 +83,7 @@ const createObjectLikeInterface = (factory: Factory.Type) => {
});
};

export const create = (factory: Factory.Type): ts.Statement[] => {
export const create = (factory: Factory.Type, list: CodeGeneratorParams[]): ts.Statement[] => {
const objectLikeOrAnyType = factory.UnionTypeNode.create({
typeNodes: [
factory.TypeReferenceNode.create({
Expand Down Expand Up @@ -110,12 +132,25 @@ export const create = (factory: Factory.Type): ts.Statement[] => {
}),
});

const successResponseNames = list.map(item => item.responseSuccessNames).flat();

const functionType = factory.FunctionTypeNode.create({
typeParameters: undefined,
typeParameters: [
factory.TypeParameterDeclaration.create({
name: "T",
defaultType: factory.TypeReferenceNode.create({
name: "SuccessResponses",
}),
}),
],
parameters: [httpMethod, url, headers, requestBody, queryParameters, options],
type: factory.TypeReferenceNode.create({
name: "Promise",
typeArguments: [factory.TypeNode.create({ type: "any" })],
typeArguments: [
factory.TypeReferenceNode.create({
name: "T",
}),
],
}),
});

Expand All @@ -129,6 +164,7 @@ export const create = (factory: Factory.Type): ts.Statement[] => {
createHttpMethod(factory),
createObjectLikeInterface(factory),
...createQueryParamsDeclarations(factory),
createSuccessResponseTypeAlias("SuccessResponses", factory, successResponseNames),
factory.InterfaceDeclaration.create({
export: true,
name: "ApiClient",
Expand Down
2 changes: 1 addition & 1 deletion src/DefaultCodeTemplate/ApiClientClass/Method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const generateResponseReturnType = (factory: Factory.Type, successResponseNameLi
});
}

// レスすポンスが存在しないので Promise<void>
// レスポンスが存在しないので Promise<void>
if (successResponseNameList.length === 0) {
return factory.TypeReferenceNode.create({
name: "Promise",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const create = (factory: Factory.Type, params: CodeGeneratorParams): ts.C

return factory.CallExpression.create({
expression: expression,
typeArguments: [],
argumentsArray: argumentsArray,
});
};
2 changes: 1 addition & 1 deletion src/DefaultCodeTemplate/ApiClientClass/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export const create = (factory: Factory.Type, list: CodeGeneratorParams[]): ts.S
return Method.create(factory, params);
});
const members = [Constructor.create(factory), ...methodList];
return [...ApiClientInterface.create(factory), Class.create(factory, members)];
return [...ApiClientInterface.create(factory, list), Class.create(factory, members)];
};
2 changes: 1 addition & 1 deletion src/DefaultCodeTemplate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as Converter from "../Converter";
import * as ApiClientArgument from "./ApiClientArgument";
import * as ApiClientClass from "./ApiClientClass";

export const makeClientApiClient: Converter.v3.Generator.MakeApiClientFunction = (
export const rewriteCodeAfterTypeDeclaration: Converter.v3.Generator.RewriteCodeAfterTypeDeclaration = (
context: ts.TransformationContext,
codeGeneratorParamsList: Converter.v3.CodeGeneratorParams[],
): ts.Statement[] => {
Expand Down
11 changes: 5 additions & 6 deletions src/DefaultCodeTemplate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,11 @@ export const generateObjectLiteralExpression = (
});
};

export const stringToArray = (text: string, delimiter: string): string[] => {
const list = text.split(delimiter);
return list.reduce<string[]>((current, item, index) => {
return index < list.length - 1 ? current.concat([item, delimiter]) : current.concat([item]);
}, []);
};
/**
* "/{a}/b/{a}/c{a}/".split(new RegExp("({a})"))
* => ["/", "{a}", "/b/", "{a}", "/c", "{a}", "/"]
*/
export const stringToArray = (text: string, delimiter: string): string[] => text.split(new RegExp(`(${delimiter})`));

export const multiSplitStringToArray = (text: string, delimiters: string[]): string[] => {
return delimiters.reduce<string[]>(
Expand Down
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export { Converter };

export interface Params {
entryPoint: string;
option?: Partial<Converter.v3.Option>;
option?: {
rewriteCodeAfterTypeDeclaration?: Converter.v3.Generator.RewriteCodeAfterTypeDeclaration;
};
/** default: true */
enableValidate?: boolean;
log?: {
Expand All @@ -28,8 +30,8 @@ export const generateTypeScriptCode = ({ entryPoint, option, enableValidate = tr
}

const convertOption: Converter.v3.Option = option
? { makeApiClient: option.makeApiClient || DefaultCodeTemplate.makeClientApiClient }
: { makeApiClient: DefaultCodeTemplate.makeClientApiClient };
? { rewriteCodeAfterTypeDeclaration: option.rewriteCodeAfterTypeDeclaration || DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration }
: { rewriteCodeAfterTypeDeclaration: DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration };
const { createFunction, generateLeadingComment } = Converter.v3.create(entryPoint, schema, resolvedReferenceDocument, convertOption);
return [generateLeadingComment(), TypeScriptCodeGenerator.generate(createFunction)].join(EOL + EOL + EOL);
};
3 changes: 2 additions & 1 deletion test/__tests__/__snapshots__/snapshot-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,9 @@ export interface QueryParameter {
export interface QueryParameters {
[key: string]: QueryParameter;
}
export type SuccessResponses = Response$getIncludeLocalReference$Status$200 | Response$getFullRemoteReference$Status$200 | Response$getReferenceItems$Status$200;
export interface ApiClient<RequestOption> {
request: (httpMethod: HttpMethod, url: string, headers: ObjectLike | any, requestBody: ObjectLike | any, queryParameters: QueryParameters | undefined, options?: RequestOption) => Promise<any>;
request: <T = SuccessResponses>(httpMethod: HttpMethod, url: string, headers: ObjectLike | any, requestBody: ObjectLike | any, queryParameters: QueryParameters | undefined, options?: RequestOption) => Promise<T>;
}
export class Client<RequestOption> {
constructor(private apiClient: ApiClient<RequestOption>, private baseUrl: string) { }
Expand Down

0 comments on commit 1c6e748

Please sign in to comment.