From 509409adcb07553246e15a005d262d580f006387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Carbonne?= Date: Wed, 23 May 2018 17:29:02 +0200 Subject: [PATCH 1/2] fixed an error when schema was type `string` schema": { "type": "string", "format": "url" }, The previous code was trying to make a `JSON.parse` on a string --- .../resources/TypescriptBrowser/api.mustache | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/resources/TypescriptBrowser/api.mustache b/src/main/resources/TypescriptBrowser/api.mustache index 7aff8e6..e95bb18 100644 --- a/src/main/resources/TypescriptBrowser/api.mustache +++ b/src/main/resources/TypescriptBrowser/api.mustache @@ -54,9 +54,12 @@ export class BaseAPI { const { url, init } = this.createFetchParams(context); const response = await this.fetchApi(url, init); if (response.status >= 200 && response.status < 300) { - if (context.responseType === 'JSON') { - const result = await response.json() as T; - return transformPropertyNames(result, context.modelPropertyNaming); + switch(context.responseType) { + case 'JSON': + const result = await response.json() as T; + return transformPropertyNames(result, context.modelPropertyNaming); + case 'text': + return await response.text() as any as T; } return response as any as T; } @@ -95,6 +98,26 @@ export class BaseAPI { } return response; } + + /* + https://swagger.io/docs/specification/2-0/describing-responses/ + + The schema keyword is used to describe the response body. A schema can define: + object or array – typically used with JSON and XML APIs, + a primitive such as a number or string – used for plain text responses, + file (see below). + */ + static getResponseType(returnType: string): 'JSON' | 'text' | undefined { + switch (returnType) { + case 'string': + return 'text' + case 'number': // JSON.parse("42.42") -> 42.42 + case 'object': + case 'array': + default: + return 'JSON' + } + } }; export class RequiredError extends Error { @@ -264,7 +287,7 @@ export class {{classname}} extends BaseAPI { body: formData, {{/hasFormParams}} {{#returnType}} - responseType: 'JSON', + responseType: BaseAPI.getResponseType('{{returnType}}'), {{/returnType}} modelPropertyNaming: '{{modelPropertyNaming}}', }); @@ -338,7 +361,7 @@ interface RequestOpts { headers: HTTPHeaders; query?: HTTPQuery; body?: HTTPBody; - responseType?: 'JSON'; + responseType?: 'JSON' | 'text'; modelPropertyNaming: ModelPropertyNaming; } From d34557f2225435b43829c02d1255b06fb1de4ee0 Mon Sep 17 00:00:00 2001 From: James Batt Date: Thu, 24 May 2018 13:42:01 +1000 Subject: [PATCH 2/2] minor changes --- .gitignore | 1 + example/api.ts | 139 +++++++++++++++--- petstore-example.sh | 2 +- .../resources/TypescriptBrowser/api.mustache | 29 ++-- 4 files changed, 133 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 580478a..405eadb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ bin/ .settings/ .classpath .project +.factorypath swagger-codegen-cli-2.3.1.jar diff --git a/example/api.ts b/example/api.ts index 33704b4..2c2c44a 100644 --- a/example/api.ts +++ b/example/api.ts @@ -192,19 +192,26 @@ export class BaseAPI { const { url, init } = this.createFetchParams(context); const response = await this.fetchApi(url, init); if (response.status >= 200 && response.status < 300) { - if (context.responseType === 'JSON') { - const result = await response.json() as T; - return transformPropertyNames(result, context.modelPropertyNaming); + switch(context.responseType) { + case 'JSON': + const result = await response.json() as T; + return transformPropertyNames(result, context.modelPropertyNaming); + case 'text': + return await response.text() as any as T; + default: + return response as any as T; } - return response as any as T; } throw response; } private createFetchParams(context: RequestOpts) { let url = this.configuration.basePath + context.path; - if (context.query !== undefined) { - url += '?' + querystring(context.query); + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + querystring(context.query); } const body = context.body instanceof FormData ? context.body : JSON.stringify(context.body); const init = { @@ -230,10 +237,26 @@ export class BaseAPI { } return response; } + + /** + * https://swagger.io/docs/specification/2-0/describing-responses/ + * + * If the response type for a given API is a 'string' we need to avoid + * parsing the response as json because JSON.parse("some string") will + * fail when the string isn't actually JSON. + */ + protected getResponseType(returnType: string): ResponseType { + switch (returnType) { + case 'string': + return 'text' + default: + return 'JSON' + } + } }; export class RequiredError extends Error { - name: "RequiredError" + name = 'RequiredError'; constructor(public field: string, msg?: string) { super(msg); } @@ -258,6 +281,15 @@ export class PetApi extends BaseAPI { headerParameters['Content-Type'] = 'application/json'; + if (this.configuration && this.configuration.accessToken) { + // oauth required + if (typeof this.configuration.accessToken === 'function') { + headerParameters["Authorization"] = this.configuration.accessToken("petstore_auth", ["write:pets", "read:pets"]); + } else { + headerParameters["Authorization"] = this.configuration.accessToken; + } + } + return this.request({ path: `/pet`, method: 'POST', @@ -278,6 +310,19 @@ export class PetApi extends BaseAPI { const headerParameters: HTTPHeaders = {}; + if (requestParameters.apiKey !== undefined && requestParameters.apiKey !== null) { + headerParameters['api_key'] = String(requestParameters.apiKey); + } + + if (this.configuration && this.configuration.accessToken) { + // oauth required + if (typeof this.configuration.accessToken === 'function') { + headerParameters["Authorization"] = this.configuration.accessToken("petstore_auth", ["write:pets", "read:pets"]); + } else { + headerParameters["Authorization"] = this.configuration.accessToken; + } + } + return this.request({ path: `/pet/{petId}`.replace(`{${"petId"}}`, encodeURIComponent(String(requestParameters.petId))), method: 'DELETE', @@ -303,12 +348,21 @@ export class PetApi extends BaseAPI { const headerParameters: HTTPHeaders = {}; + if (this.configuration && this.configuration.accessToken) { + // oauth required + if (typeof this.configuration.accessToken === 'function') { + headerParameters["Authorization"] = this.configuration.accessToken("petstore_auth", ["write:pets", "read:pets"]); + } else { + headerParameters["Authorization"] = this.configuration.accessToken; + } + } + return this.request>({ path: `/pet/findByStatus`, method: 'GET', headers: headerParameters, query: queryParameters, - responseType: 'JSON', + responseType: this.getResponseType('Array'), modelPropertyNaming: 'camelCase', }); } @@ -330,12 +384,21 @@ export class PetApi extends BaseAPI { const headerParameters: HTTPHeaders = {}; + if (this.configuration && this.configuration.accessToken) { + // oauth required + if (typeof this.configuration.accessToken === 'function') { + headerParameters["Authorization"] = this.configuration.accessToken("petstore_auth", ["write:pets", "read:pets"]); + } else { + headerParameters["Authorization"] = this.configuration.accessToken; + } + } + return this.request>({ path: `/pet/findByTags`, method: 'GET', headers: headerParameters, query: queryParameters, - responseType: 'JSON', + responseType: this.getResponseType('Array'), modelPropertyNaming: 'camelCase', }); } @@ -351,11 +414,15 @@ export class PetApi extends BaseAPI { const headerParameters: HTTPHeaders = {}; + if (this.configuration && this.configuration.apiKey) { + headerParameters["api_key"] = this.configuration.apiKey("api_key"); // api_key authentication + } + return this.request({ path: `/pet/{petId}`.replace(`{${"petId"}}`, encodeURIComponent(String(requestParameters.petId))), method: 'GET', headers: headerParameters, - responseType: 'JSON', + responseType: this.getResponseType('Pet'), modelPropertyNaming: 'camelCase', }); } @@ -373,6 +440,15 @@ export class PetApi extends BaseAPI { headerParameters['Content-Type'] = 'application/json'; + if (this.configuration && this.configuration.accessToken) { + // oauth required + if (typeof this.configuration.accessToken === 'function') { + headerParameters["Authorization"] = this.configuration.accessToken("petstore_auth", ["write:pets", "read:pets"]); + } else { + headerParameters["Authorization"] = this.configuration.accessToken; + } + } + return this.request({ path: `/pet`, method: 'PUT', @@ -458,7 +534,7 @@ export class PetApi extends BaseAPI { method: 'POST', headers: headerParameters, body: formData, - responseType: 'JSON', + responseType: this.getResponseType('ApiResponse'), modelPropertyNaming: 'camelCase', }); } @@ -496,11 +572,15 @@ export class StoreApi extends BaseAPI { async getInventory(): Promise<{ [key: string]: number; }> { const headerParameters: HTTPHeaders = {}; + if (this.configuration && this.configuration.apiKey) { + headerParameters["api_key"] = this.configuration.apiKey("api_key"); // api_key authentication + } + return this.request<{ [key: string]: number; }>({ path: `/store/inventory`, method: 'GET', headers: headerParameters, - responseType: 'JSON', + responseType: this.getResponseType('{ [key: string]: number; }'), modelPropertyNaming: 'camelCase', }); } @@ -520,7 +600,7 @@ export class StoreApi extends BaseAPI { path: `/store/order/{orderId}`.replace(`{${"orderId"}}`, encodeURIComponent(String(requestParameters.orderId))), method: 'GET', headers: headerParameters, - responseType: 'JSON', + responseType: this.getResponseType('Order'), modelPropertyNaming: 'camelCase', }); } @@ -543,7 +623,7 @@ export class StoreApi extends BaseAPI { method: 'POST', headers: headerParameters, body: requestParameters.body, - responseType: 'JSON', + responseType: this.getResponseType('Order'), modelPropertyNaming: 'camelCase', }); } @@ -655,7 +735,7 @@ export class UserApi extends BaseAPI { path: `/user/{username}`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), method: 'GET', headers: headerParameters, - responseType: 'JSON', + responseType: this.getResponseType('User'), modelPropertyNaming: 'camelCase', }); } @@ -690,7 +770,7 @@ export class UserApi extends BaseAPI { method: 'GET', headers: headerParameters, query: queryParameters, - responseType: 'JSON', + responseType: this.getResponseType('string'), modelPropertyNaming: 'camelCase', }); } @@ -775,11 +855,12 @@ export class Configuration { this.middleware = conf.middleware || []; this.username = conf.username; this.password = conf.password; - if (conf.apiKey) { - this.apiKey = typeof conf.apiKey === 'function' ? conf.apiKey : () => conf.apiKey; + const { apiKey, accessToken } = conf; + if (apiKey) { + this.apiKey = typeof apiKey === 'function' ? apiKey : () => apiKey; } - if (conf.accessToken) { - this.accessToken = typeof conf.accessToken === 'function' ? conf.accessToken : () => conf.accessToken; + if (accessToken) { + this.accessToken = typeof accessToken === 'function' ? accessToken : () => accessToken; } } } @@ -796,13 +877,15 @@ export interface FetchParams { init: RequestInit; } +type ResponseType = 'JSON' | 'text'; + interface RequestOpts { path: string; method: HTTPMethod; headers: HTTPHeaders; query?: HTTPQuery; body?: HTTPBody; - responseType?: 'JSON'; + responseType?: ResponseType; modelPropertyNaming: ModelPropertyNaming; } @@ -824,8 +907,20 @@ export interface Middleware { post?(fetch: FetchAPI, url: string, init: RequestInit, response: Response): Promise; } +function capitalize(word: string) { + return word.charAt(0).toUpperCase() + word.slice(1); +} + +function toPascalCase(name: string) { + return name + .split('_') + .map(capitalize) + .join(''); +} + function toCamelCase(name: string) { - return (name.charAt(0).toLowerCase() + name.slice(1) || name).toString(); + const pascalCase = toPascalCase(name); + return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1); } function applyPropertyNameConverter(json: any, converter: (name: string) => string) { diff --git a/petstore-example.sh b/petstore-example.sh index 1889548..ce6bc98 100755 --- a/petstore-example.sh +++ b/petstore-example.sh @@ -3,7 +3,7 @@ set -e mvn clean package -CUSTOM_GEN_JAR=./target/TypescriptBrowser-swagger-codegen-0.0.1-shaded.jar +CUSTOM_GEN_JAR=./target/TypescriptBrowser-swagger-codegen-*-shaded.jar java -jar ${CUSTOM_GEN_JAR} generate \ -l TypescriptBrowser \ diff --git a/src/main/resources/TypescriptBrowser/api.mustache b/src/main/resources/TypescriptBrowser/api.mustache index e95bb18..fed356f 100644 --- a/src/main/resources/TypescriptBrowser/api.mustache +++ b/src/main/resources/TypescriptBrowser/api.mustache @@ -60,8 +60,9 @@ export class BaseAPI { return transformPropertyNames(result, context.modelPropertyNaming); case 'text': return await response.text() as any as T; + default: + return response as any as T; } - return response as any as T; } throw response; } @@ -99,21 +100,17 @@ export class BaseAPI { return response; } - /* - https://swagger.io/docs/specification/2-0/describing-responses/ - - The schema keyword is used to describe the response body. A schema can define: - object or array – typically used with JSON and XML APIs, - a primitive such as a number or string – used for plain text responses, - file (see below). - */ - static getResponseType(returnType: string): 'JSON' | 'text' | undefined { + /** + * https://swagger.io/docs/specification/2-0/describing-responses/ + * + * If the response type for a given API is a 'string' we need to avoid + * parsing the response as json because JSON.parse("some string") will + * fail when the string isn't actually JSON. + */ + protected getResponseType(returnType: string): ResponseType { switch (returnType) { case 'string': return 'text' - case 'number': // JSON.parse("42.42") -> 42.42 - case 'object': - case 'array': default: return 'JSON' } @@ -287,7 +284,7 @@ export class {{classname}} extends BaseAPI { body: formData, {{/hasFormParams}} {{#returnType}} - responseType: BaseAPI.getResponseType('{{returnType}}'), + responseType: this.getResponseType('{{{returnType}}}'), {{/returnType}} modelPropertyNaming: '{{modelPropertyNaming}}', }); @@ -355,13 +352,15 @@ export interface FetchParams { init: RequestInit; } +type ResponseType = 'JSON' | 'text'; + interface RequestOpts { path: string; method: HTTPMethod; headers: HTTPHeaders; query?: HTTPQuery; body?: HTTPBody; - responseType?: 'JSON' | 'text'; + responseType?: ResponseType; modelPropertyNaming: ModelPropertyNaming; }