ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Typescript] swagger로 httpClient code를 자동 생성해주는 도구 swagger-typescript-api 소개
    Javascript & TypeScript 2023. 1. 29. 11:53
    반응형

    Swagger?

    스웨거(Swagger)는 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다. 대부분의 사용자들은 스웨거 UI 도구를 통해 스웨거를 식별하며 스웨거 툴셋에는 자동화된 문서화, 코드 생성, 테스트 케이스 생성 지원이 포함된다.

    스웨거 (소프트웨어) - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

     

    swagger-typescript-api

    swagger-typescript-api는 별다른 추가 설정 없이 현재 구현중인 typescript 프로젝트 내 적절한 위치에 추가시킬 수 있는 CLI 명령어 도구이다.

     

    acacode/swagger-typescript-api: TypeScript API generator via Swagger scheme (github.com)

     

    GitHub - acacode/swagger-typescript-api: TypeScript API generator via Swagger scheme

    TypeScript API generator via Swagger scheme. Contribute to acacode/swagger-typescript-api development by creating an account on GitHub.

    github.com

     

    typescript 기반 프로젝트 위치에서 아래 명령어를 입력하면 ts 소스코드가 생성된다.

    npx swagger-typescript-api -p {swagger 파일경로, url도 가능} -o {생성할위치} -n {생성할파일명}.ts --api-class-name {생성할 클래스명}

     

     

    사용방법 예시

    아래 코드는 예제로 만든 swagger 내용이다.

    {
      "openapi": "3.0.1",
      "info": {
        "title": "날씨 API",
        "version": "v1"
      },
      "paths": {
        "/WeatherForecast": {
          "get": {
            "tags": [
              "WeatherForecast"
            ],
            "summary": "날씨 데이터를 모두 조회합니다.",
            "operationId": "GetWeatherForecast",
            "responses": {
              "200": {
                "description": "Success",
                "content": {
                  "text/plain": {
                    "schema": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WeatherForecast"
                      }
                    }
                  },
                  "application/json": {
                    "schema": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WeatherForecast"
                      }
                    }
                  },
                  "text/json": {
                    "schema": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WeatherForecast"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "/WeatherForecast/{day}": {
          "get": {
            "tags": [
              "WeatherForecast"
            ],
            "summary": "입력받은 day 값 이후에 대한 날씨 데이터를 조회합니다.",
            "operationId": "GetWeatherForecastById",
            "parameters": [
              {
                "name": "day",
                "in": "path",
                "description": "날짜값입니다.",
                "required": true,
                "style": "simple",
                "schema": {
                  "type": "integer",
                  "format": "int32"
                },
                "example": 5
              }
            ],
            "responses": {
              "200": {
                "description": "Success",
                "content": {
                  "text/plain": {
                    "schema": {
                      "$ref": "#/components/schemas/WeatherForecast"
                    }
                  },
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/WeatherForecast"
                    }
                  },
                  "text/json": {
                    "schema": {
                      "$ref": "#/components/schemas/WeatherForecast"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "components": {
        "schemas": {
          "DateOnly": {
            "type": "object",
            "properties": {
              "year": {
                "type": "integer",
                "format": "int32"
              },
              "month": {
                "type": "integer",
                "format": "int32"
              },
              "day": {
                "type": "integer",
                "format": "int32"
              },
              "dayOfWeek": {
                "$ref": "#/components/schemas/DayOfWeek"
              },
              "dayOfYear": {
                "type": "integer",
                "format": "int32",
                "readOnly": true
              },
              "dayNumber": {
                "type": "integer",
                "format": "int32",
                "readOnly": true
              }
            },
            "additionalProperties": false
          },
          "DayOfWeek": {
            "enum": [
              0,
              1,
              2,
              3,
              4,
              5,
              6
            ],
            "type": "integer",
            "format": "int32"
          },
          "WeatherForecast": {
            "type": "object",
            "properties": {
              "date": {
                "$ref": "#/components/schemas/DateOnly"
              },
              "temperatureC": {
                "type": "integer",
                "description": "섭씨",
                "format": "int32",
                "example": 1
              },
              "temperatureF": {
                "type": "integer",
                "description": "화씨",
                "format": "int32",
                "readOnly": true
              },
              "summary": {
                "type": "string",
                "description": "날씨 요약",
                "nullable": true,
                "example": "Cool"
              }
            },
            "additionalProperties": false,
            "description": "날씨 DTO"
          }
        }
      },
      "tags": [
        {
          "name": "WeatherForecast",
          "description": "날씨 조회 컨트롤러"
        }
      ]
    }

    해당 내용을 swagger.json으로 생성할 코드 위치에 만든 뒤, 아래 명령어를 입력한다.

    npx swagger-typescript-api -p swagger.json -o ./src/lib -n weatherApi.ts --api-class-name WeatherApi

    명령어 실행위치 내 /src/lib 디렉토리에 weatherApi.ts 가 생성된 것을 확인할 수 있다.

    /* eslint-disable */
    /* tslint:disable */
    /*
     * ---------------------------------------------------------------
     * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API        ##
     * ##                                                           ##
     * ## AUTHOR: acacode                                           ##
     * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
     * ---------------------------------------------------------------
     */
    
    export interface DateOnly {
    	/** @format int32 */
    	year?: number;
    	/** @format int32 */
    	month?: number;
    	/** @format int32 */
    	day?: number;
    	dayOfWeek?: DayOfWeek;
    	/** @format int32 */
    	dayOfYear?: number;
    	/** @format int32 */
    	dayNumber?: number;
    }
    
    /** @format int32 */
    export enum DayOfWeek {
    	Value0 = 0,
    	Value1 = 1,
    	Value2 = 2,
    	Value3 = 3,
    	Value4 = 4,
    	Value5 = 5,
    	Value6 = 6
    }
    
    /** 날씨 DTO */
    export interface WeatherForecast {
    	date?: DateOnly;
    	/**
    	 * 섭씨
    	 * @format int32
    	 * @example 1
    	 */
    	temperatureC?: number;
    	/**
    	 * 화씨
    	 * @format int32
    	 */
    	temperatureF?: number;
    	/**
    	 * 날씨 요약
    	 * @example "Cool"
    	 */
    	summary?: string | null;
    }
    
    export type QueryParamsType = Record<string | number, any>;
    export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'>;
    
    export interface FullRequestParams extends Omit<RequestInit, 'body'> {
    	/** set parameter to `true` for call `securityWorker` for this request */
    	secure?: boolean;
    	/** request path */
    	path: string;
    	/** content type of request body */
    	type?: ContentType;
    	/** query params */
    	query?: QueryParamsType;
    	/** format of response (i.e. response.json() -> format: "json") */
    	format?: ResponseFormat;
    	/** request body */
    	body?: unknown;
    	/** base url */
    	baseUrl?: string;
    	/** request cancellation token */
    	cancelToken?: CancelToken;
    }
    
    export type RequestParams = Omit<FullRequestParams, 'body' | 'method' | 'query' | 'path'>;
    
    export interface ApiConfig<SecurityDataType = unknown> {
    	baseUrl?: string;
    	baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'>;
    	securityWorker?: (
    		securityData: SecurityDataType | null
    	) => Promise<RequestParams | void> | RequestParams | void;
    	customFetch?: typeof fetch;
    }
    
    export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
    	data: D;
    	error: E;
    }
    
    type CancelToken = Symbol | string | number;
    
    export enum ContentType {
    	Json = 'application/json',
    	FormData = 'multipart/form-data',
    	UrlEncoded = 'application/x-www-form-urlencoded',
    	Text = 'text/plain'
    }
    
    export class HttpClient<SecurityDataType = unknown> {
    	public baseUrl: string = '';
    	private securityData: SecurityDataType | null = null;
    	private securityWorker?: ApiConfig<SecurityDataType>['securityWorker'];
    	private abortControllers = new Map<CancelToken, AbortController>();
    	private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
    
    	private baseApiParams: RequestParams = {
    		credentials: 'same-origin',
    		headers: {},
    		redirect: 'follow',
    		referrerPolicy: 'no-referrer'
    	};
    
    	constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
    		Object.assign(this, apiConfig);
    	}
    
    	public setSecurityData = (data: SecurityDataType | null) => {
    		this.securityData = data;
    	};
    
    	protected encodeQueryParam(key: string, value: any) {
    		const encodedKey = encodeURIComponent(key);
    		return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}`;
    	}
    
    	protected addQueryParam(query: QueryParamsType, key: string) {
    		return this.encodeQueryParam(key, query[key]);
    	}
    
    	protected addArrayQueryParam(query: QueryParamsType, key: string) {
    		const value = query[key];
    		return value.map((v: any) => this.encodeQueryParam(key, v)).join('&');
    	}
    
    	protected toQueryString(rawQuery?: QueryParamsType): string {
    		const query = rawQuery || {};
    		const keys = Object.keys(query).filter((key) => 'undefined' !== typeof query[key]);
    		return keys
    			.map((key) =>
    				Array.isArray(query[key])
    					? this.addArrayQueryParam(query, key)
    					: this.addQueryParam(query, key)
    			)
    			.join('&');
    	}
    
    	protected addQueryParams(rawQuery?: QueryParamsType): string {
    		const queryString = this.toQueryString(rawQuery);
    		return queryString ? `?${queryString}` : '';
    	}
    
    	private contentFormatters: Record<ContentType, (input: any) => any> = {
    		[ContentType.Json]: (input: any) =>
    			input !== null && (typeof input === 'object' || typeof input === 'string')
    				? JSON.stringify(input)
    				: input,
    		[ContentType.Text]: (input: any) =>
    			input !== null && typeof input !== 'string' ? JSON.stringify(input) : input,
    		[ContentType.FormData]: (input: any) =>
    			Object.keys(input || {}).reduce((formData, key) => {
    				const property = input[key];
    				formData.append(
    					key,
    					property instanceof Blob
    						? property
    						: typeof property === 'object' && property !== null
    						? JSON.stringify(property)
    						: `${property}`
    				);
    				return formData;
    			}, new FormData()),
    		[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input)
    	};
    
    	protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
    		return {
    			...this.baseApiParams,
    			...params1,
    			...(params2 || {}),
    			headers: {
    				...(this.baseApiParams.headers || {}),
    				...(params1.headers || {}),
    				...((params2 && params2.headers) || {})
    			}
    		};
    	}
    
    	protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
    		if (this.abortControllers.has(cancelToken)) {
    			const abortController = this.abortControllers.get(cancelToken);
    			if (abortController) {
    				return abortController.signal;
    			}
    			return void 0;
    		}
    
    		const abortController = new AbortController();
    		this.abortControllers.set(cancelToken, abortController);
    		return abortController.signal;
    	};
    
    	public abortRequest = (cancelToken: CancelToken) => {
    		const abortController = this.abortControllers.get(cancelToken);
    
    		if (abortController) {
    			abortController.abort();
    			this.abortControllers.delete(cancelToken);
    		}
    	};
    
    	public request = async <T = any, E = any>({
    		body,
    		secure,
    		path,
    		type,
    		query,
    		format,
    		baseUrl,
    		cancelToken,
    		...params
    	}: FullRequestParams): Promise<HttpResponse<T, E>> => {
    		const secureParams =
    			((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) &&
    				this.securityWorker &&
    				(await this.securityWorker(this.securityData))) ||
    			{};
    		const requestParams = this.mergeRequestParams(params, secureParams);
    		const queryString = query && this.toQueryString(query);
    		const payloadFormatter = this.contentFormatters[type || ContentType.Json];
    		const responseFormat = format || requestParams.format;
    
    		return this.customFetch(
    			`${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`,
    			{
    				...requestParams,
    				headers: {
    					...(requestParams.headers || {}),
    					...(type && type !== ContentType.FormData ? { 'Content-Type': type } : {})
    				},
    				signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal,
    				body: typeof body === 'undefined' || body === null ? null : payloadFormatter(body)
    			}
    		).then(async (response) => {
    			const r = response as HttpResponse<T, E>;
    			r.data = null as unknown as T;
    			r.error = null as unknown as E;
    
    			const data = !responseFormat
    				? r
    				: await response[responseFormat]()
    						.then((data) => {
    							if (r.ok) {
    								r.data = data;
    							} else {
    								r.error = data;
    							}
    							return r;
    						})
    						.catch((e) => {
    							r.error = e;
    							return r;
    						});
    
    			if (cancelToken) {
    				this.abortControllers.delete(cancelToken);
    			}
    
    			if (!response.ok) throw data;
    			return data;
    		});
    	};
    }
    
    /**
     * @title 날씨 API
     * @version v1
     */
    export class WeatherApi<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
    	weatherForecast = {
    		/**
    		 * No description
    		 *
    		 * @tags WeatherForecast
    		 * @name GetWeatherForecast
    		 * @summary 날씨 데이터를 모두 조회합니다.
    		 * @request GET:/WeatherForecast
    		 */
    		getWeatherForecast: (params: RequestParams = {}) =>
    			this.request<WeatherForecast[], any>({
    				path: `/WeatherForecast`,
    				method: 'GET',
    				format: 'json',
    				...params
    			}),
    
    		/**
    		 * No description
    		 *
    		 * @tags WeatherForecast
    		 * @name GetWeatherForecastById
    		 * @summary 입력받은 day 값 이후에 대한 날씨 데이터를 조회합니다.
    		 * @request GET:/WeatherForecast/{day}
    		 */
    		getWeatherForecastById: (day: number, params: RequestParams = {}) =>
    			this.request<WeatherForecast, any>({
    				path: `/WeatherForecast/${day}`,
    				method: 'GET',
    				format: 'json',
    				...params
    			})
    	};
    }

    해당코드를 호출할 코드에서 import 하여 사용하면 된다.

        import { WeatherApi, type WeatherForecast } from "../lib/weatherApi";
        const { weatherForecast } = new WeatherApi({
            baseUrl : "https://localhost:7032"
        });
        
        let weather : WeatherForecast | undefined = undefined;
        
        
        // weatherForecast.getWeatherForecast() // 적절한 위치에서 호출하면 된다.
        // weatherForecast.getWeatherForecastById(1) // 적절한 위치에서 호출하면 된다.

     

    예제

    아래 예제는 테스트를 위해 직접 생성한 코드이다.

    backend 가 닷넷 기반이기 때문에 닷넷을 사용하지 않는 개발자가 실행하는데 애로사항이 있을 수 있지만 이해하는덴 도움이 될 것이다.

    frontend는 sveltekit 기반이지만 별도의 global 도구 없이 npm i 로 설치 및 실행 가능할 것이다.

     

    ddochea0314/example-typescript-swaggergen: swagger-typescript-api 자동생성도구 예제 (github.com)

     

    GitHub - ddochea0314/example-typescript-swaggergen: swagger-typescript-api 자동생성도구 예제

    swagger-typescript-api 자동생성도구 예제. Contribute to ddochea0314/example-typescript-swaggergen development by creating an account on GitHub.

    github.com

     

    반응형

    댓글

Designed by Tistory.