Skip to content

Commit

Permalink
feat(codegen): add error response type name list and add allowOperati…
Browse files Browse the repository at this point in the history
…onIds (#20)

* feat(comment): Use the defined comment
* chore(codegen): add error response names
* test: update snapshot
* feat: add error response namespace
* feat: add allow operationId list
  • Loading branch information
Himenon committed Mar 22, 2021
1 parent 7d50829 commit 924c4a1
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 12 deletions.
33 changes: 26 additions & 7 deletions src/Converter/v3/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const extractPickedParameter = (parameter: OpenApi.Parameter): PickedParameter =
};
};

const extractSuccessStatusCode = (responses: { [statusCode: string]: OpenApi.Response }): string[] => {
const extractResponseNamesByStatusCode = (type: "success" | "error", responses: { [statusCode: string]: OpenApi.Response }): string[] => {
const statusCodeList: string[] = [];
Object.entries(responses || {}).forEach(([statusCodeLike, response]) => {
// ContentTypeの定義が存在しない場合はstatusCodeを読み取らない
Expand All @@ -23,8 +23,14 @@ const extractSuccessStatusCode = (responses: { [statusCode: string]: OpenApi.Res
}
if (typeof statusCodeLike === "string") {
const statusCodeNumberValue = parseInt(statusCodeLike, 10);
if (200 <= statusCodeNumberValue && statusCodeNumberValue < 300) {
statusCodeList.push(statusCodeNumberValue.toString());
if (type === "success") {
if (200 <= statusCodeNumberValue && statusCodeNumberValue < 300) {
statusCodeList.push(statusCodeNumberValue.toString());
}
} else if (type === "error") {
if (400 <= statusCodeNumberValue && statusCodeNumberValue < 600) {
statusCodeList.push(statusCodeNumberValue.toString());
}
}
}
});
Expand All @@ -37,7 +43,7 @@ const getRequestContentTypeList = (requestBody: OpenApi.RequestBody): string[] =

const getSuccessResponseContentTypeList = (responses: { [statusCode: string]: OpenApi.Response }): string[] => {
let contentTypeList: string[] = [];
extractSuccessStatusCode(responses).forEach(statusCode => {
extractResponseNamesByStatusCode("success", responses).forEach(statusCode => {
const response = responses[statusCode];
contentTypeList = contentTypeList.concat(Object.keys(response.content || {}));
});
Expand All @@ -51,11 +57,21 @@ const hasQueryParameters = (parameters?: OpenApi.Parameter[]): boolean => {
return parameters.filter(parameter => parameter.in === "query").length > 0;
};

const generateCodeGeneratorParamsList = (store: Store.Type, converterContext: ConverterContext.Types): CodeGeneratorParams[] => {
const generateCodeGeneratorParamsList = (
store: Store.Type,
converterContext: ConverterContext.Types,
allowOperationIds: string[] | undefined,
): CodeGeneratorParams[] => {
const operationState = store.getNoReferenceOperationState();
const params: CodeGeneratorParams[] = [];
Object.entries(operationState).forEach(([operationId, item]) => {
const responseSuccessNames = extractSuccessStatusCode(item.responses).map(statusCode =>
if (allowOperationIds && !allowOperationIds.includes(operationId)) {
return;
}
const responseSuccessNames = extractResponseNamesByStatusCode("success", item.responses).map(statusCode =>
converterContext.generateResponseName(operationId, statusCode),
);
const responseErrorNames = extractResponseNamesByStatusCode("error", item.responses).map(statusCode =>
converterContext.generateResponseName(operationId, statusCode),
);
const requestContentTypeList = item.requestBody ? getRequestContentTypeList(item.requestBody) : [];
Expand All @@ -65,6 +81,7 @@ const generateCodeGeneratorParamsList = (store: Store.Type, converterContext: Co

const formatParams: CodeGeneratorParams = {
operationId: operationId,
escapedOperationId: converterContext.escapeOperationIdText(operationId),
rawRequestUri: item.requestUri,
httpMethod: item.httpMethod,
argumentParamsTypeDeclaration: converterContext.generateArgumentParamsTypeDeclaration(operationId),
Expand All @@ -88,6 +105,7 @@ const generateCodeGeneratorParamsList = (store: Store.Type, converterContext: Co
responseSuccessNames: responseSuccessNames,
responseFirstSuccessName: responseSuccessNames.length === 1 ? responseSuccessNames[0] : undefined,
has2OrMoreSuccessNames: hasOver2SuccessNames,
responseErrorNames: responseErrorNames,
// Response Success Content Type
successResponseContentTypes: responseSuccessContentTypes,
successResponseFirstContentType: responseSuccessContentTypes.length === 1 ? responseSuccessContentTypes[0] : undefined,
Expand All @@ -113,7 +131,8 @@ export const generateApiClientCode = (
context: ts.TransformationContext,
converterContext: ConverterContext.Types,
rewriteCodeAfterTypeDeclaration: RewriteCodeAfterTypeDeclaration,
allowOperationIds: string[] | undefined,
): void => {
const codeGeneratorParamsList = generateCodeGeneratorParamsList(store, converterContext);
const codeGeneratorParamsList = generateCodeGeneratorParamsList(store, converterContext, allowOperationIds);
store.addAdditionalStatement(rewriteCodeAfterTypeDeclaration(context, codeGeneratorParamsList));
};
4 changes: 3 additions & 1 deletion src/Converter/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface Option {
* It is possible to rewrite the implementation after the type declaration.
*/
rewriteCodeAfterTypeDeclaration: Generator.RewriteCodeAfterTypeDeclaration;

allowOperationIds?: string[];
}

export const create = (entryPoint: string, rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document, option: Option): Type => {
Expand Down Expand Up @@ -97,7 +99,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.rewriteCodeAfterTypeDeclaration);
Generator.generateApiClientCode(store, context, converterContext, option.rewriteCodeAfterTypeDeclaration, option.allowOperationIds);
}
return store.getRootStatements();
};
Expand Down
3 changes: 3 additions & 0 deletions src/Converter/v3/types/CodeGeneratorParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type PickedParameter = Pick<OpenApi.Parameter, "name" | "in" | "required"

export interface CodeGeneratorParams {
operationId: string;
escapedOperationId: string;
httpMethod: string; // get, post, put, delete ...etc
rawRequestUri: string;
functionName: string;
Expand All @@ -19,6 +20,8 @@ export interface CodeGeneratorParams {
requestContentTypes: string[];
requestFirstContentType: string | undefined; // requestContentTypes.length === 1 only
has2OrMoreRequestContentTypes: boolean; // requestContentTypes.length > 1
// Response Error Response Name
responseErrorNames: string[];
// Response Success Name
responseSuccessNames: string[]; // `Response$${operationId}$Status$${statusCode}`[]
responseFirstSuccessName: string | undefined; // responseSuccessNames.length === 1 only
Expand Down
30 changes: 30 additions & 0 deletions src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ import { CodeGeneratorParams } from "../../Converter/v3";

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

const createErrorResponsesTypeAlias = (typeName: string, factory: Factory.Type, errorResponseNames: string[]) => {
if (errorResponseNames.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: errorResponseNames.map(name => {
return factory.TypeReferenceNode.create({
name,
});
}),
}),
});
};

const createSuccessResponseTypeAlias = (typeName: string, factory: Factory.Type, successResponseNames: string[]) => {
if (successResponseNames.length === 0) {
return factory.TypeAliasDeclaration.create({
Expand Down Expand Up @@ -134,6 +155,14 @@ export const create = (factory: Factory.Type, list: CodeGeneratorParams[]): ts.S

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

const errorResponseNamespace = factory.Namespace.create({
export: true,
name: "ErrorResponse",
statements: list.map(item => {
return createErrorResponsesTypeAlias(`${item.escapedOperationId}`, factory, item.responseErrorNames);
}),
});

const functionType = factory.FunctionTypeNode.create({
typeParameters: [
factory.TypeParameterDeclaration.create({
Expand Down Expand Up @@ -165,6 +194,7 @@ export const create = (factory: Factory.Type, list: CodeGeneratorParams[]): ts.S
createObjectLikeInterface(factory),
...createQueryParamsDeclarations(factory),
createSuccessResponseTypeAlias("SuccessResponses", factory, successResponseNames),
errorResponseNamespace,
factory.InterfaceDeclaration.create({
export: true,
name: "ApiClient",
Expand Down
23 changes: 19 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ export interface Params {
/** default: true */
enableValidate?: boolean;
log?: {
validator?: Validator.v3.LogOption;
validator?: {
/**
* default: undefined (all logs)
* Number of lines displayed in the latest log
*/
displayLogLines?: number;
};
};
filter?: {
allowOperationIds?: string[];
};
}

export const generateTypeScriptCode = ({ entryPoint, option, enableValidate = true, log }: Params): string => {
export const generateTypeScriptCode = ({ entryPoint, option, enableValidate = true, log, filter = {} }: Params): string => {
const schema = fileSystem.loadJsonOrYaml(entryPoint);
const resolvedReferenceDocument = ResolveReference.resolve(entryPoint, entryPoint, JSON.parse(JSON.stringify(schema)));

Expand All @@ -30,8 +39,14 @@ export const generateTypeScriptCode = ({ entryPoint, option, enableValidate = tr
}

const convertOption: Converter.v3.Option = option
? { rewriteCodeAfterTypeDeclaration: option.rewriteCodeAfterTypeDeclaration || DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration }
: { rewriteCodeAfterTypeDeclaration: DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration };
? {
rewriteCodeAfterTypeDeclaration: option.rewriteCodeAfterTypeDeclaration || DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration,
allowOperationIds: filter.allowOperationIds,
}
: {
rewriteCodeAfterTypeDeclaration: DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration,
allowOperationIds: filter.allowOperationIds,
};
const { createFunction, generateLeadingComment } = Converter.v3.create(entryPoint, schema, resolvedReferenceDocument, convertOption);
return [generateLeadingComment(), TypeScriptCodeGenerator.generate(createFunction)].join(EOL + EOL + EOL);
};
6 changes: 6 additions & 0 deletions test/__tests__/__snapshots__/snapshot-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ export interface QueryParameters {
[key: string]: QueryParameter;
}
export type SuccessResponses = Response$getIncludeLocalReference$Status$200 | Response$getFullRemoteReference$Status$200 | Response$getReferenceItems$Status$200;
export namespace ErrorResponse {
export type getIncludeLocalReference = void;
export type getIncludeRemoteReference = void;
export type getFullRemoteReference = void;
export type getReferenceItems = void;
}
export interface ApiClient<RequestOption> {
request: <T = SuccessResponses>(httpMethod: HttpMethod, url: string, headers: ObjectLike | any, requestBody: ObjectLike | any, queryParameters: QueryParameters | undefined, options?: RequestOption) => Promise<T>;
}
Expand Down

0 comments on commit 924c4a1

Please sign in to comment.