From b62cc9599828de070c58e3bd4ca5035654976a21 Mon Sep 17 00:00:00 2001 From: Guillaume FORTAINE Date: Wed, 4 Jan 2023 18:20:39 +0100 Subject: [PATCH] chore(openapi): use typescript generator w/ fetch --- config.yaml | 10 + openapi.yaml | 8 + scripts/generate_sdk.py | 8 +- .../typescript-axios/apiInner.mustache | 364 ------------------ .../typescript-axios/configuration.mustache | 121 ------ .../typescript/api/api.mustache | 253 ++++++++++++ .../typescript/auth/auth.mustache | 178 +++++++++ .../typescript/configuration.mustache | 82 ++++ .../typescript/http/http.mustache | 354 +++++++++++++++++ .../typescript/http/isomorphic-fetch.mustache | 56 +++ .../typescript/http/servers.mustache | 56 +++ .../typescript/logger.mustache | 6 + .../model/ObjectSerializer.mustache | 356 +++++++++++++++++ .../typescript/model/model.mustache | 84 ++++ .../typescript/package.mustache | 87 +++++ .../typescript/tsconfig.mustache | 45 +++ .../typescript/util.mustache | 135 +++++++ 17 files changed, 1716 insertions(+), 487 deletions(-) create mode 100644 config.yaml delete mode 100644 sdk-template-overrides/typescript-axios/apiInner.mustache delete mode 100644 sdk-template-overrides/typescript-axios/configuration.mustache create mode 100644 sdk-template-overrides/typescript/api/api.mustache create mode 100644 sdk-template-overrides/typescript/auth/auth.mustache create mode 100644 sdk-template-overrides/typescript/configuration.mustache create mode 100644 sdk-template-overrides/typescript/http/http.mustache create mode 100644 sdk-template-overrides/typescript/http/isomorphic-fetch.mustache create mode 100644 sdk-template-overrides/typescript/http/servers.mustache create mode 100644 sdk-template-overrides/typescript/logger.mustache create mode 100644 sdk-template-overrides/typescript/model/ObjectSerializer.mustache create mode 100644 sdk-template-overrides/typescript/model/model.mustache create mode 100644 sdk-template-overrides/typescript/package.mustache create mode 100644 sdk-template-overrides/typescript/tsconfig.mustache create mode 100644 sdk-template-overrides/typescript/util.mustache diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..5d66f376 --- /dev/null +++ b/config.yaml @@ -0,0 +1,10 @@ +additionalProperties: + npmName: "@fortaine/openai" + npmVersion: "4.0.0" + supportsES6: true + extensionForDeno: .js + platform: node +files: + logger.mustache: + templateType: SupportingFiles + destinationFilename: logger.ts \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml index a0b74fd1..783bbc25 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3124,6 +3124,14 @@ components: - created_at - level - message + securitySchemes: + apiKeyAuth: + type: http + scheme: bearer + bearerFormat: JWT + +security: + - apiKeyAuth: [] x-oaiMeta: groups: diff --git a/scripts/generate_sdk.py b/scripts/generate_sdk.py index 78d2c8be..f7e469d5 100644 --- a/scripts/generate_sdk.py +++ b/scripts/generate_sdk.py @@ -84,10 +84,14 @@ def generate_sanitized_spec(sanitized_spec_path): def generate_sdk(sanitized_spec_path, sdk_type, output_path): """Use openapi-generator to generate the SDK.""" if sdk_type == "node": + sanitized_spec_path = sanitized_spec_path.replace(" ", "\\ ") + output_path = output_path.replace(" ", "\\ ") template_override_path = os.path.join(os.path.dirname( - __file__), "../sdk-template-overrides/typescript-axios") + __file__), "../sdk-template-overrides/typescript").replace(" ", "\\ ") + configuration_path = os.path.join(os.path.dirname( + __file__), "../config.yaml").replace(" ", "\\ ") os.system( - f"openapi-generator generate -i {sanitized_spec_path} -g typescript-axios -o {output_path} -p supportsES6=true -t {template_override_path}") + f"openapi-generator generate -i {sanitized_spec_path} -g typescript -o {output_path} -t {template_override_path} -c {configuration_path}") else: print(f"Unsupported SDK type {sdk_type}, skipping SDK generation") diff --git a/sdk-template-overrides/typescript-axios/apiInner.mustache b/sdk-template-overrides/typescript-axios/apiInner.mustache deleted file mode 100644 index d39525bf..00000000 --- a/sdk-template-overrides/typescript-axios/apiInner.mustache +++ /dev/null @@ -1,364 +0,0 @@ -{{#withSeparateModelsAndApi}} -/* tslint:disable */ -/* eslint-disable */ -{{>licenseInfo}} - -import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; -import { Configuration } from '{{apiRelativeToRoot}}configuration'; -// Some imports not used depending on template conditions -// @ts-ignore -import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common'; -// @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '{{apiRelativeToRoot}}base'; -{{#imports}} -// @ts-ignore -import { {{classname}} } from '{{apiRelativeToRoot}}{{tsModelPackage}}'; -{{/imports}} -{{/withSeparateModelsAndApi}} -{{^withSeparateModelsAndApi}} -{{/withSeparateModelsAndApi}} -{{#operations}} -/** - * {{classname}} - axios parameter creator{{#description}} - * {{&description}}{{/description}} - * @export - */ -export const {{classname}}AxiosParamCreator = function (configuration?: Configuration) { - return { - {{#operation}} - /** - * {{¬es}} - {{#summary}} - * @summary {{&summary}} - {{/summary}} - {{#allParams}} - * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}} - {{/allParams}} - * @param {*} [options] Override http request option.{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - * @throws {RequiredError} - */ - {{nickname}}: async ({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options: AxiosRequestConfig = {}): Promise => { - {{#allParams}} - {{#required}} - // verify required parameter '{{paramName}}' is not null or undefined - assertParamExists('{{nickname}}', '{{paramName}}', {{paramName}}) - {{/required}} - {{/allParams}} - const localVarPath = `{{{path}}}`{{#pathParams}} - .replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}}; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}} - const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}} - - {{#authMethods}} - // authentication {{name}} required - {{#isApiKey}} - {{#isKeyInHeader}} - await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration) - {{/isKeyInHeader}} - {{#isKeyInQuery}} - await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration) - {{/isKeyInQuery}} - {{/isApiKey}} - {{#isBasicBasic}} - // http basic authentication required - setBasicAuthToObject(localVarRequestOptions, configuration) - {{/isBasicBasic}} - {{#isBasicBearer}} - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - {{/isBasicBearer}} - {{#isOAuth}} - // oauth required - await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration) - {{/isOAuth}} - - {{/authMethods}} - {{#queryParams}} - {{#isArray}} - if ({{paramName}}) { - {{#isCollectionFormatMulti}} - {{#uniqueItems}} - localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}); - {{/uniqueItems}} - {{^uniqueItems}} - localVarQueryParameter['{{baseName}}'] = {{paramName}}; - {{/uniqueItems}} - {{/isCollectionFormatMulti}} - {{^isCollectionFormatMulti}} - {{#uniqueItems}} - localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}).join(COLLECTION_FORMATS.{{collectionFormat}}); - {{/uniqueItems}} - {{^uniqueItems}} - localVarQueryParameter['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}}); - {{/uniqueItems}} - {{/isCollectionFormatMulti}} - } - {{/isArray}} - {{^isArray}} - if ({{paramName}} !== undefined) { - {{#isDateTime}} - localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ? - ({{paramName}} as any).toISOString() : - {{paramName}}; - {{/isDateTime}} - {{^isDateTime}} - {{#isDate}} - localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ? - ({{paramName}} as any).toISOString().substr(0,10) : - {{paramName}}; - {{/isDate}} - {{^isDate}} - localVarQueryParameter['{{baseName}}'] = {{paramName}}; - {{/isDate}} - {{/isDateTime}} - } - {{/isArray}} - - {{/queryParams}} - {{#headerParams}} - {{#isArray}} - if ({{paramName}}) { - {{#uniqueItems}} - let mapped = Array.from({{paramName}}).map(value => ("{{{dataType}}}" !== "Set") ? JSON.stringify(value) : (value || "")); - {{/uniqueItems}} - {{^uniqueItems}} - let mapped = {{paramName}}.map(value => ("{{{dataType}}}" !== "Array") ? JSON.stringify(value) : (value || "")); - {{/uniqueItems}} - localVarHeaderParameter['{{baseName}}'] = mapped.join(COLLECTION_FORMATS["{{collectionFormat}}"]); - } - {{/isArray}} - {{^isArray}} - if ({{paramName}} !== undefined && {{paramName}} !== null) { - {{#isString}} - localVarHeaderParameter['{{baseName}}'] = String({{paramName}}); - {{/isString}} - {{^isString}} - localVarHeaderParameter['{{baseName}}'] = String(JSON.stringify({{paramName}})); - {{/isString}} - } - {{/isArray}} - - {{/headerParams}} - {{#vendorExtensions}} - {{#formParams}} - {{#isArray}} - if ({{paramName}}) { - {{#isCollectionFormatMulti}} - {{paramName}}.forEach((element) => { - localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', element as any); - }) - {{/isCollectionFormatMulti}} - {{^isCollectionFormatMulti}} - localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}})); - {{/isCollectionFormatMulti}} - }{{/isArray}} - {{^isArray}} - if ({{paramName}} !== undefined) { {{^multipartFormData}} - localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}} - localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}} - localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isPrimitiveType}}{{/multipartFormData}} - }{{/isArray}} - {{/formParams}}{{/vendorExtensions}} - {{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}} - localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}} - localVarHeaderParameter['Content-Type'] = 'multipart/form-data';{{/multipartFormData}} - {{/hasFormParams}}{{/vendorExtensions}} - {{#bodyParam}} - {{^consumes}} - localVarHeaderParameter['Content-Type'] = 'application/json'; - {{/consumes}} - {{#consumes.0}} - localVarHeaderParameter['Content-Type'] = '{{{mediaType}}}'; - {{/consumes.0}} - - {{/bodyParam}} - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - {{! Edited by OpenAI, line 188 }} - localVarRequestOptions.headers = {...localVarHeaderParameter,{{#vendorExtensions}}{{#multipartFormData}} ...localVarFormParams.getHeaders(),{{/multipartFormData}}{{/vendorExtensions}} ...headersFromBaseOptions, ...options.headers}; - {{#hasFormParams}} - localVarRequestOptions.data = localVarFormParams{{#vendorExtensions}}{{^multipartFormData}}.toString(){{/multipartFormData}}{{/vendorExtensions}}; - {{/hasFormParams}} - {{#bodyParam}} - localVarRequestOptions.data = serializeDataIfNeeded({{paramName}}, localVarRequestOptions, configuration) - {{/bodyParam}} - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - {{/operation}} - } -}; - -/** - * {{classname}} - functional programming interface{{#description}} - * {{{.}}}{{/description}} - * @export - */ -export const {{classname}}Fp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = {{classname}}AxiosParamCreator(configuration) - return { - {{#operation}} - /** - * {{¬es}} - {{#summary}} - * @summary {{&summary}} - {{/summary}} - {{#allParams}} - * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}} - {{/allParams}} - * @param {*} [options] Override http request option.{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - * @throws {RequiredError} - */ - async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, - {{/operation}} - } -}; - -/** - * {{classname}} - factory interface{{#description}} - * {{&description}}{{/description}} - * @export - */ -export const {{classname}}Factory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = {{classname}}Fp(configuration) - return { - {{#operation}} - /** - * {{¬es}} - {{#summary}} - * @summary {{&summary}} - {{/summary}} - {{#allParams}} - * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}} - {{/allParams}} - * @param {*} [options] Override http request option.{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - * @throws {RequiredError} - */ - {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: any): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> { - return localVarFp.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(axios, basePath)); - }, - {{/operation}} - }; -}; - -{{#withInterfaces}} -/** - * {{classname}} - interface{{#description}} - * {{&description}}{{/description}} - * @export - * @interface {{classname}} - */ -export interface {{classname}}Interface { -{{#operation}} - /** - * {{¬es}} - {{#summary}} - * @summary {{&summary}} - {{/summary}} - {{#allParams}} - * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}} - {{/allParams}} - * @param {*} [options] Override http request option.{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - * @throws {RequiredError} - * @memberof {{classname}}Interface - */ - {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>; - -{{/operation}} -} - -{{/withInterfaces}} -{{#useSingleRequestParameter}} -{{#operation}} -{{#allParams.0}} -/** - * Request parameters for {{nickname}} operation in {{classname}}. - * @export - * @interface {{classname}}{{operationIdCamelCase}}Request - */ -export interface {{classname}}{{operationIdCamelCase}}Request { - {{#allParams}} - /** - * {{description}} - * @type {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> - * @memberof {{classname}}{{operationIdCamelCase}} - */ - readonly {{paramName}}{{^required}}?{{/required}}: {{{dataType}}} - {{^-last}} - - {{/-last}} - {{/allParams}} -} - -{{/allParams.0}} -{{/operation}} -{{/useSingleRequestParameter}} -/** - * {{classname}} - object-oriented interface{{#description}} - * {{{.}}}{{/description}} - * @export - * @class {{classname}} - * @extends {BaseAPI} - */ -{{#withInterfaces}} -export class {{classname}} extends BaseAPI implements {{classname}}Interface { -{{/withInterfaces}} -{{^withInterfaces}} -export class {{classname}} extends BaseAPI { -{{/withInterfaces}} - {{#operation}} - /** - * {{¬es}} - {{#summary}} - * @summary {{&summary}} - {{/summary}} - {{#useSingleRequestParameter}} - {{#allParams.0}} - * @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters. - {{/allParams.0}} - {{/useSingleRequestParameter}} - {{^useSingleRequestParameter}} - {{#allParams}} - * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}} - {{/allParams}} - {{/useSingleRequestParameter}} - * @param {*} [options] Override http request option.{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - * @throws {RequiredError} - * @memberof {{classname}} - */ - {{#useSingleRequestParameter}} - public {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig) { - return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(this.axios, this.basePath)); - } - {{/useSingleRequestParameter}} - {{^useSingleRequestParameter}} - public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig) { - return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(this.axios, this.basePath)); - } - {{/useSingleRequestParameter}} - {{^-last}} - - {{/-last}} - {{/operation}} -} -{{/operations}} \ No newline at end of file diff --git a/sdk-template-overrides/typescript-axios/configuration.mustache b/sdk-template-overrides/typescript-axios/configuration.mustache deleted file mode 100644 index 1020f66b..00000000 --- a/sdk-template-overrides/typescript-axios/configuration.mustache +++ /dev/null @@ -1,121 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -{{>licenseInfo}} - -{{! Edited by OpenAI, line 6 }} -const packageJson = require("../package.json"); - -export interface ConfigurationParameters { - apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); - {{! Edited by OpenAI, line 1 }} - organization?: string; - username?: string; - password?: string; - accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); - basePath?: string; - baseOptions?: any; - formDataCtor?: new () => any; -} - -export class Configuration { - /** - * parameter for apiKey security - * @param name security name - * @memberof Configuration - */ - apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); - {{! Edited by OpenAI, lines 28-34 }} - /** - * OpenAI organization id - * - * @type {string} - * @memberof Configuration - */ - organization?: string; - /** - * parameter for basic security - * - * @type {string} - * @memberof Configuration - */ - username?: string; - /** - * parameter for basic security - * - * @type {string} - * @memberof Configuration - */ - password?: string; - /** - * parameter for oauth2 security - * @param name security name - * @param scopes oauth2 scope - * @memberof Configuration - */ - accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); - /** - * override base path - * - * @type {string} - * @memberof Configuration - */ - basePath?: string; - /** - * base options for axios calls - * - * @type {any} - * @memberof Configuration - */ - baseOptions?: any; - /** - * The FormData constructor that will be used to create multipart form data - * requests. You can inject this here so that execution environments that - * do not support the FormData class can still run the generated client. - * - * @type {new () => FormData} - */ - formDataCtor?: new () => any; - - constructor(param: ConfigurationParameters = {}) { - this.apiKey = param.apiKey; - {{! Edited by OpenAI, line 82 }} - this.organization = param.organization; - this.username = param.username; - this.password = param.password; - this.accessToken = param.accessToken; - this.basePath = param.basePath; - this.baseOptions = param.baseOptions; - this.formDataCtor = param.formDataCtor; - - {{! Edited by OpenAI, lines 91-104 }} - if (!this.baseOptions) { - this.baseOptions = {}; - } - this.baseOptions.headers = { - 'User-Agent': `OpenAI/NodeJS/${packageJson.version}`, - 'Authorization': `Bearer ${this.apiKey}`, - ...this.baseOptions.headers, - } - if (this.organization) { - this.baseOptions.headers['OpenAI-Organization'] = this.organization; - } - if (!this.formDataCtor) { - this.formDataCtor = require("form-data"); - } - } - - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * @param mime - MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - public isJsonMime(mime: string): boolean { - const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); - return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); - } -} \ No newline at end of file diff --git a/sdk-template-overrides/typescript/api/api.mustache b/sdk-template-overrides/typescript/api/api.mustache new file mode 100644 index 00000000..f2210c7f --- /dev/null +++ b/sdk-template-overrides/typescript/api/api.mustache @@ -0,0 +1,253 @@ +// TODO: better import syntax? +import {BaseAPIRequestFactory, RequiredError, COLLECTION_FORMATS} from './baseapi{{extensionForDeno}}'; +import {Configuration} from '../configuration{{extensionForDeno}}'; +import {RequestContext, HttpMethod, ResponseContext, HttpFile} from '../http/http{{extensionForDeno}}'; +{{#platforms}} +{{#node}} +import {{^supportsES6}}* as{{/supportsES6}} FormData from "form-data"; +import { URLSearchParams } from 'url'; +{{/node}} +{{/platforms}} +import {ObjectSerializer} from '../models/ObjectSerializer{{extensionForDeno}}'; +import {ApiException} from './exception{{extensionForDeno}}'; +import {canConsumeForm, isCodeInRange} from '../util{{extensionForDeno}}'; +import {SecurityAuthentication} from '../auth/auth{{extensionForDeno}}'; + +{{#useInversify}} +import { injectable } from "inversify"; +{{/useInversify}} + +{{#imports}} +import { {{classname}} } from '{{filename}}{{extensionForDeno}}'; +{{/imports}} +{{#operations}} + +/** + * {{{description}}}{{^description}}no description{{/description}} + */ +{{#useInversify}} +@injectable() +{{/useInversify}} +export class {{classname}}RequestFactory extends BaseAPIRequestFactory { + + {{#operation}} + /** + {{#notes}} + * {{¬es}} + {{/notes}} + {{#summary}} + * {{&summary}} + {{/summary}} + {{#allParams}} + * @param {{paramName}} {{description}} + {{/allParams}} + */ + public async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}_options?: Configuration): Promise { + let _config = _options || this.configuration; + {{#allParams}} + + {{#required}} + // verify required parameter '{{paramName}}' is not null or undefined + if ({{paramName}} === null || {{paramName}} === undefined) { + throw new RequiredError("{{classname}}", "{{nickname}}", "{{paramName}}"); + } + + {{/required}} + {{/allParams}} + + // Path Params + const localVarPath = '{{{path}}}'{{#pathParams}} + .replace('{' + '{{baseName}}' + '}', encodeURIComponent(String({{paramName}}))){{/pathParams}}; + + // Make Request Context + const requestContext = _config.baseServer.makeRequestContext(localVarPath, HttpMethod.{{httpMethod}}, _config); + requestContext.setHeaderParam("Accept", "application/json, */*;q=0.8") + {{#queryParams}} + + // Query Params + if ({{paramName}} !== undefined) { + requestContext.setQueryParam("{{baseName}}", ObjectSerializer.serialize({{paramName}}, "{{{dataType}}}", "{{dataFormat}}")); + } + {{/queryParams}} + {{#headerParams}} + + // Header Params + requestContext.setHeaderParam("{{baseName}}", ObjectSerializer.serialize({{paramName}}, "{{{dataType}}}", "{{dataFormat}}")); + {{/headerParams}} + {{#hasFormParams}} + + // Form Params + const useForm = canConsumeForm([ + {{#consumes}} + '{{{mediaType}}}', + {{/consumes}} + ]); + + let localVarFormParams + if (useForm) { + localVarFormParams = new FormData(); + } else { + localVarFormParams = new URLSearchParams(); + } + {{/hasFormParams}} + + {{#formParams}} + {{#isArray}} + if ({{paramName}}) { + {{#isCollectionFormatMulti}} + {{paramName}}.forEach((element) => { + localVarFormParams.append('{{baseName}}', element as any); + }) + {{/isCollectionFormatMulti}} + {{^isCollectionFormatMulti}} + // TODO: replace .append with .set + localVarFormParams.append('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS["{{collectionFormat}}"])); + {{/isCollectionFormatMulti}} + } + {{/isArray}} + {{^isArray}} + if ({{paramName}} !== undefined) { + // TODO: replace .append with .set + {{^isFile}} + localVarFormParams.append('{{baseName}}', {{paramName}} as any); + {{/isFile}} + {{#isFile}} + if (localVarFormParams instanceof FormData) { + {{#platforms}} + {{#node}} + localVarFormParams.append('{{baseName}}', {{paramName}}.data, {{paramName}}.name); + {{/node}} + {{^node}} + localVarFormParams.append('{{baseName}}', {{paramName}}, {{paramName}}.name); + {{/node}} + {{/platforms}} + } + {{/isFile}} + } + {{/isArray}} + {{/formParams}} + {{#hasFormParams}} + + requestContext.setBody(localVarFormParams); + + if(!useForm) { + const contentType = ObjectSerializer.getPreferredMediaType([{{#consumes}} + "{{{mediaType}}}"{{^-last}},{{/-last}} + {{/consumes}}]); + requestContext.setHeaderParam("Content-Type", contentType); + } + {{/hasFormParams}} + {{#bodyParam}} + + // Body Params + const contentType = ObjectSerializer.getPreferredMediaType([{{#consumes}} + "{{{mediaType}}}"{{^-last}},{{/-last}} + {{/consumes}}]); + requestContext.setHeaderParam("Content-Type", contentType); + const serializedBody = ObjectSerializer.stringify( + ObjectSerializer.serialize({{paramName}}, "{{{dataType}}}", "{{dataFormat}}"), + contentType + ); + requestContext.setBody(serializedBody); + {{/bodyParam}} + + {{#hasAuthMethods}} + let authMethod: SecurityAuthentication | undefined; + {{/hasAuthMethods}} + {{#authMethods}} + // Apply auth methods + authMethod = _config.authMethods["{{name}}"] + if (authMethod?.applySecurityAuthentication) { + await authMethod?.applySecurityAuthentication(requestContext); + } + {{/authMethods}} + + {{^useInversify}} + const defaultAuth: SecurityAuthentication | undefined = _options?.authMethods?.default || this.configuration?.authMethods?.default + if (defaultAuth?.applySecurityAuthentication) { + await defaultAuth?.applySecurityAuthentication(requestContext); + } + {{/useInversify}} + + return requestContext; + } + + {{/operation}} +} +{{/operations}} +{{#operations}} + +{{#useInversify}} +@injectable() +{{/useInversify}} +export class {{classname}}ResponseProcessor { + + {{#operation}} + /** + * Unwraps the actual response sent by the server from the response context and deserializes the response content + * to the expected objects + * + * @params response Response returned by the server for a request to {{nickname}} + * @throws ApiException if the response code was not in [200, 299] + */ + public async {{nickname}}(response: ResponseContext): Promise<{{{returnType}}} {{^returnType}}void{{/returnType}}> { + const contentType = ObjectSerializer.normalizeMediaType(response.headers["content-type"]); + {{#responses}} + if (isCodeInRange("{{code}}", response.httpStatusCode)) { + {{#dataType}} + {{#isBinary}} + const body: {{{dataType}}} = await response.getBodyAsFile() as unknown as {{{returnType}}}; + {{/isBinary}} + {{^isBinary}} + {{#is2xx}} + if (contentType === "text/event-stream") return response.body.stream() as unknown as {{{dataType}}}; + {{/is2xx}} + const body: {{{dataType}}} = ObjectSerializer.deserialize( + ObjectSerializer.parse(await response.body.text(), contentType), + "{{{dataType}}}", "{{returnFormat}}" + ) as {{{dataType}}}; + {{/isBinary}} + {{#is2xx}} + return body; + {{/is2xx}} + {{^is2xx}} + throw new ApiException<{{{dataType}}}>(response.httpStatusCode, "{{message}}", body, response.headers); + {{/is2xx}} + {{/dataType}} + {{^dataType}} + {{#is2xx}} + return; + {{/is2xx}} + {{^is2xx}} + throw new ApiException(response.httpStatusCode, "{{message}}", undefined, response.headers); + {{/is2xx}} + {{/dataType}} + } + {{/responses}} + + // Work around for missing responses in specification, e.g. for petstore.yaml + if (response.httpStatusCode >= 200 && response.httpStatusCode <= 299) { + {{#returnType}} + {{#isBinary}} + const body: {{{returnType}}} = await response.getBodyAsFile() as unknown as {{{returnType}}}; + {{/isBinary}} + {{^isBinary}} + if (contentType === "text/event-stream") return response.body.stream() as unknown as {{{returnType}}}; + const body: {{{returnType}}} = ObjectSerializer.deserialize( + ObjectSerializer.parse(await response.body.text(), contentType), + "{{{returnType}}}", "{{returnFormat}}" + ) as {{{returnType}}}; + {{/isBinary}} + return body; + {{/returnType}} + {{^returnType}} + return; + {{/returnType}} + } + + throw new ApiException(response.httpStatusCode, "Unknown API Status Code!", await response.getBodyAsAny(), response.headers); + } + + {{/operation}} +} +{{/operations}} diff --git a/sdk-template-overrides/typescript/auth/auth.mustache b/sdk-template-overrides/typescript/auth/auth.mustache new file mode 100644 index 00000000..fdca0a30 --- /dev/null +++ b/sdk-template-overrides/typescript/auth/auth.mustache @@ -0,0 +1,178 @@ +{{#platforms}} +{{#node}} +// typings for btoa are incorrect +//@ts-ignore +import {{^supportsES6}}* as{{/supportsES6}} btoa from "btoa"; +{{/node}} +{{/platforms}} +import { RequestContext } from "../http/http{{extensionForDeno}}"; +{{#useInversify}} +import { injectable, inject, named } from "inversify"; +import { AbstractTokenProvider } from "../services/configuration"; +{{/useInversify}} + +/** + * Interface authentication schemes. + */ +export interface SecurityAuthentication { + /* + * @return returns the name of the security authentication as specified in OAI + */ + getName(): string; + + /** + * Applies the authentication scheme to the request context + * + * @params context the request context which should use this authentication scheme + */ + applySecurityAuthentication(context: RequestContext): void | Promise; +} + +{{#useInversify}} +export const AuthApiKey = Symbol("auth.api_key"); +export const AuthUsername = Symbol("auth.username"); +export const AuthPassword = Symbol("auth.password"); + +{{/useInversify}} +export interface TokenProvider { + getToken(): Promise | string; +} + +{{#authMethods}} +/** + * Applies {{type}} authentication to the request context. + */ +{{#useInversify}} +@injectable() +{{/useInversify}} +export class {{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}Authentication implements SecurityAuthentication { + {{#isApiKey}} + /** + * Configures this api key authentication with the necessary properties + * + * @param apiKey: The api key to be used for every request + */ + public constructor({{#useInversify}}@inject(AuthApiKey) @named("{{name}}") {{/useInversify}}private apiKey: string) {} + {{/isApiKey}} + {{#isBasicBasic}} + /** + * Configures the http authentication with the required details. + * + * @param username username for http basic authentication + * @param password password for http basic authentication + */ + public constructor( + {{#useInversify}}@inject(AuthUsername) @named("{{name}}") {{/useInversify}}private username: string, + {{#useInversify}}@inject(AuthPassword) @named("{{name}}") {{/useInversify}}private password: string + ) {} + {{/isBasicBasic}} + {{#isOAuth}} + /** + * Configures the http authentication with the required details. + * + * @param tokenProvider service that can provide the up-to-date token when needed + */ + public constructor({{#useInversify}}@inject(AbstractTokenProvider) @named("{{name}}") {{/useInversify}}private tokenProvider: TokenProvider) {} + {{/isOAuth}} + {{#isBasicBearer}} + /** + * Configures OAuth2 with the necessary properties + * + * @param accessToken: The access token to be used for every request + */ + public constructor(private accessToken: string) {} + {{/isBasicBearer}} + + public getName(): string { + return "{{name}}"; + } + + public {{#isOAuth}}async {{/isOAuth}}applySecurityAuthentication(context: RequestContext) { + {{#isApiKey}} + context.{{#isKeyInHeader}}setHeaderParam{{/isKeyInHeader}}{{#isKeyInQuery}}setQueryParam{{/isKeyInQuery}}{{#isKeyInCookie}}addCookie{{/isKeyInCookie}}("{{keyParamName}}", this.apiKey); + {{/isApiKey}} + {{#isBasicBasic}} + let comb = this.username + ":" + this.password; + context.setHeaderParam("Authorization", "Basic " + btoa(comb)); + {{/isBasicBasic}} + {{#isOAuth}} + context.setHeaderParam("Authorization", "Bearer " + await this.tokenProvider.getToken()); + {{/isOAuth}} + {{#isBasicBearer}} + context.setHeaderParam("Authorization", "Bearer " + this.accessToken); + {{/isBasicBearer}} + } +} + +{{/authMethods}} + +export type AuthMethods = { + {{^useInversify}} + "default"?: SecurityAuthentication, + {{/useInversify}} + {{#authMethods}} + "{{name}}"?: SecurityAuthentication{{^-last}},{{/-last}} + {{/authMethods}} +} +{{#useInversify}} + +export const authMethodServices = { + {{^useInversify}} + "default"?: SecurityAuthentication, + {{/useInversify}} + {{#authMethods}} + "{{name}}": {{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}Authentication{{^-last}},{{/-last}} + {{/authMethods}} +} +{{/useInversify}} + +export type ApiKeyConfiguration = string; +export type HttpBasicConfiguration = { "username": string, "password": string }; +export type HttpBearerConfiguration = { tokenProvider: TokenProvider }; +export type OAuth2Configuration = { accessToken: string }; + +export type AuthMethodsConfiguration = { + {{^useInversify}} + "default"?: SecurityAuthentication, + {{/useInversify}} + {{#authMethods}} + "{{name}}"?: {{#isApiKey}}ApiKeyConfiguration{{/isApiKey}}{{#isBasicBasic}}HttpBasicConfiguration{{/isBasicBasic}}{{#isOAuth}}HttpBearerConfiguration{{/isOAuth}}{{#isBasicBearer}}OAuth2Configuration{{/isBasicBearer}}{{^-last}},{{/-last}} + {{/authMethods}} +} + +/** + * Creates the authentication methods from a swagger description. + * + */ +export function configureAuthMethods(config: AuthMethodsConfiguration | undefined): AuthMethods { + let authMethods: AuthMethods = {} + + if (!config) { + return authMethods; + } + {{^useInversify}} + authMethods["default"] = config["default"] + {{/useInversify}} + + {{#authMethods}} + if (config["{{name}}"]) { + authMethods["{{name}}"] = new {{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}Authentication( + {{#isApiKey}} + config["{{name}}"] + {{/isApiKey}} + {{#isBasicBasic}} + config["{{name}}"]["username"], + config["{{name}}"]["password"] + {{/isBasicBasic}} + {{#isOAuth}} + config["{{name}}"]["tokenProvider"] + {{/isOAuth}} + {{#isBasicBearer}} + config["{{name}}"]["accessToken"] + {{/isBasicBearer}} + ); + } + + {{/authMethods}} + return authMethods; +} \ No newline at end of file diff --git a/sdk-template-overrides/typescript/configuration.mustache b/sdk-template-overrides/typescript/configuration.mustache new file mode 100644 index 00000000..36ba4863 --- /dev/null +++ b/sdk-template-overrides/typescript/configuration.mustache @@ -0,0 +1,82 @@ +import { HttpLibrary } from "./http/http{{extensionForDeno}}"; +import { Middleware, PromiseMiddleware, PromiseMiddlewareWrapper } from "./middleware{{extensionForDeno}}"; +{{#frameworks}} +{{#fetch-api}} +import { IsomorphicFetchHttpLibrary as DefaultHttpLibrary } from "./http/isomorphic-fetch{{extensionForDeno}}"; +{{/fetch-api}} +{{#jquery}} +import { JQueryHttpLibrary as DefaultHttpLibrary } from "./http/jquery"; +{{/jquery}} +{{/frameworks}} +import { BaseServerConfiguration, server1 } from "./servers{{extensionForDeno}}"; +import { configureAuthMethods, AuthMethods, AuthMethodsConfiguration } from "./auth/auth{{extensionForDeno}}"; + +export interface Configuration { + readonly baseServer: BaseServerConfiguration; + readonly httpApi: HttpLibrary; + readonly middleware: Middleware[]; + readonly authMethods: AuthMethods; + readonly organization?: string; +} + + +/** + * Interface with which a configuration object can be configured. + */ +export interface ConfigurationParameters { + /** + * Default server to use + */ + baseServer?: BaseServerConfiguration; + /** + * HTTP library to use e.g. IsomorphicFetch + */ + httpApi?: HttpLibrary; + /** + * The middlewares which will be applied to requests and responses + */ + middleware?: Middleware[]; + /** + * Configures all middlewares using the promise api instead of observables (which Middleware uses) + */ + promiseMiddleware?: PromiseMiddleware[]; + /** + * Configuration for the available authentication methods + */ + authMethods?: AuthMethodsConfiguration + /** + * OpenAI organization id + * + * @type {string} + * @memberof Configuration + */ + organization?: string; +} + +/** + * Configuration factory function + * + * If a property is not included in conf, a default is used: + * - baseServer: server1 + * - httpApi: IsomorphicFetchHttpLibrary + * - middleware: [] + * - promiseMiddleware: [] + * - authMethods: {} + * + * @param conf partial configuration + */ +export function createConfiguration(conf: ConfigurationParameters = {}): Configuration { + const configuration: Configuration = { + baseServer: conf.baseServer !== undefined ? conf.baseServer : server1, + httpApi: conf.httpApi || new DefaultHttpLibrary(), + middleware: conf.middleware || [], + authMethods: configureAuthMethods(conf.authMethods), + ...(conf.organization && { organization: conf.organization }) + }; + if (conf.promiseMiddleware) { + conf.promiseMiddleware.forEach( + m => configuration.middleware.push(new PromiseMiddlewareWrapper(m)) + ); + } + return configuration; +} \ No newline at end of file diff --git a/sdk-template-overrides/typescript/http/http.mustache b/sdk-template-overrides/typescript/http/http.mustache new file mode 100644 index 00000000..5ad4d66b --- /dev/null +++ b/sdk-template-overrides/typescript/http/http.mustache @@ -0,0 +1,354 @@ +{{#platforms}} +{{#node}} +// TODO: evaluate if we can easily get rid of this library +import {{^supportsES6}}* as{{/supportsES6}} FormData from "form-data"; +import { URLSearchParams } from 'url'; +import * as http from 'http'; +import * as https from 'https'; +import filedirname from "filedirname"; +import fs from "fs"; +import path from "path"; +import { Readable } from 'node:stream'; +{{/node}} +{{/platforms}} +{{#platforms}} +{{^deno}} +import {{^supportsES6}}* as{{/supportsES6}} URLParse from "url-parse"; +{{/deno}} +{{/platforms}} +import { Observable, from } from {{#useRxJS}}'rxjs'{{/useRxJS}}{{^useRxJS}}'../rxjsStub{{extensionForDeno}}'{{/useRxJS}}; +import { Configuration } from '../configuration{{extensionForDeno}}'; + +{{#platforms}} +{{^deno}} +{{#frameworks}} +{{#fetch-api}} +export * from './isomorphic-fetch{{extensionForDeno}}'; +{{/fetch-api}} +{{#jquery}} +export * from './jquery'; +{{/jquery}} +{{/frameworks}} +{{/deno}} +{{/platforms}} + +/** + * Represents an HTTP method. + */ +export enum HttpMethod { + GET = "GET", + HEAD = "HEAD", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", + CONNECT = "CONNECT", + OPTIONS = "OPTIONS", + TRACE = "TRACE", + PATCH = "PATCH" +} + +/** + * Represents an HTTP file which will be transferred from or to a server. + */ +{{#platforms}} +{{#node}} +export type HttpFile = { + data: {{{fileContentDataType}}}, + name: string +}; + +const [, dirname] = filedirname(); +const filePath = path.join(dirname, '../../package.json'); +const contents = fs.readFileSync(filePath, { encoding: 'utf8' }); +const packageJson = JSON.parse(contents); +{{/node}} +{{^node}} +export type HttpFile = {{{fileContentDataType}}} & { readonly name: string }; +{{/node}} +{{/platforms}} + +{{#platforms}} +{{#deno}} +/** + * URLParse Wrapper for Deno + */ +class URLParse { + private url: URL; + + constructor(address: string, _parser: boolean) { + this.url = new URL(address); + } + + public set(_part: 'query', obj: {[key: string]: string | undefined}) { + for (const key in obj) { + const value = obj[key]; + if (value) { + this.url.searchParams.set(key, value); + } else { + this.url.searchParams.set(key, ""); + } + } + } + + public get query() { + const obj: {[key: string]: string} = {}; + for (const [key, value] of this.url.searchParams.entries()) { + obj[key] = value; + } + return obj; + } + + public toString() { + return this.url.toString(); + } +} +{{/deno}} +{{/platforms}} + +export class HttpException extends Error { + public constructor(msg: string) { + super(msg); + } +} + +/** + * Represents the body of an outgoing HTTP request. + */ +export type RequestBody = undefined | string | FormData | URLSearchParams; + +/** + * Represents an HTTP request context + */ +export class RequestContext { + private headers: { [key: string]: string } = {}; + private body: RequestBody = undefined; + private url: URLParse; + {{#platforms}} + {{#node}} + private agent: http.Agent | https.Agent | undefined = undefined; + {{/node}} + {{/platforms}} + + /** + * Creates the request context using a http method and request resource url + * + * @param url url of the requested resource + * @param httpMethod http method + */ + public constructor(url: string, private httpMethod: HttpMethod, configuration: Configuration) { + this.url = new URLParse(url, true); + this.headers = { + "user-agent": `OpenAI/NodeJS/${packageJson.version}`, + ...(configuration.organization && { 'OpenAI-Organization': configuration.organization }) + } + } + + /* + * Returns the url set in the constructor including the query string + * + */ + public getUrl(): string { + return this.url.toString(); + } + + /** + * Replaces the url set in the constructor with this url. + * + */ + public setUrl(url: string) { + this.url = new URLParse(url, true); + } + + /** + * Sets the body of the http request either as a string or FormData + * + * Note that setting a body on a HTTP GET, HEAD, DELETE, CONNECT or TRACE + * request is discouraged. + * https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#rfc.section.7.3.1 + * + * @param body the body of the request + */ + public setBody(body: RequestBody) { + this.body = body; + } + + public getHttpMethod(): HttpMethod { + return this.httpMethod; + } + + public getHeaders(): { [key: string]: string } { + return this.headers; + } + + public getBody(): RequestBody { + return this.body; + } + + public setQueryParam(name: string, value: string) { + let queryObj = this.url.query; + queryObj[name] = value; + this.url.set("query", queryObj); + } + + /** + * Sets a cookie with the name and value. NO check for duplicate cookies is performed + * + */ + public addCookie(name: string, value: string): void { + if (!this.headers["Cookie"]) { + this.headers["Cookie"] = ""; + } + this.headers["Cookie"] += name + "=" + value + "; "; + } + + public setHeaderParam(key: string, value: string): void { + this.headers[key] = value; + } + {{#platforms}} + {{#node}} + + public setAgent(agent: http.Agent | https.Agent) { + this.agent = agent; + } + + public getAgent(): http.Agent | https.Agent | undefined { + return this.agent; + } + {{/node}} + {{/platforms}} +} + +export interface ResponseBody { + text(): Promise; + binary(): Promise<{{{fileContentDataType}}}>; + stream(): Readable; +} + +/** + * Helper class to generate a `ResponseBody` from binary data + */ +export class SelfDecodingBody implements Omit { + constructor(private dataSource: Promise<{{{fileContentDataType}}}>) {} + + binary(): Promise<{{{fileContentDataType}}}> { + return this.dataSource; + } + + async text(): Promise { + const data: {{{fileContentDataType}}} = await this.dataSource; + {{#platforms}} + {{#node}} + return data.toString(); + {{/node}} + {{#browser}} + // @ts-ignore + if (data.text) { + // @ts-ignore + return data.text(); + } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", () => resolve(reader.result as string)); + reader.addEventListener("error", () => reject(reader.error)); + reader.readAsText(data); + }); + {{/browser}} + {{#deno}} + return data.text(); + {{/deno}} + {{/platforms}} + } +} + +export class ResponseContext { + public constructor( + public httpStatusCode: number, + public headers: { [key: string]: string }, + public body: ResponseBody + ) {} + + /** + * Parse header value in the form `value; param1="value1"` + * + * E.g. for Content-Type or Content-Disposition + * Parameter names are converted to lower case + * The first parameter is returned with the key `""` + */ + public getParsedHeader(headerName: string): { [parameter: string]: string } { + const result: { [parameter: string]: string } = {}; + if (!this.headers[headerName]) { + return result; + } + + const parameters = this.headers[headerName].split(";"); + for (const parameter of parameters) { + let [key, value] = parameter.split("=", 2); + key = key.toLowerCase().trim(); + if (value === undefined) { + result[""] = key; + } else { + value = value.trim(); + if (value.startsWith('"') && value.endsWith('"')) { + value = value.substring(1, value.length - 1); + } + result[key] = value; + } + } + return result; + } + + public async getBodyAsFile(): Promise { + const data = await this.body.binary(); + const fileName = this.getParsedHeader("content-disposition")["filename"] || ""; + {{#platforms}} + {{#node}} + return { data, name: fileName }; + {{/node}} + {{^node}} + const contentType = this.headers["content-type"] || ""; + try { + return new File([data], fileName, { type: contentType }); + } catch (error) { + /** Fallback for when the File constructor is not available */ + return Object.assign(data, { + name: fileName, + type: contentType + }); + } + {{/node}} + {{/platforms}} + } + + /** + * Use a heuristic to get a body of unknown data structure. + * Return as string if possible, otherwise as binary or stream. + */ + public getBodyAsAny(): Promise { + try { + return this.body.text(); + } catch {} + + try { + return this.body.binary(); + } catch {} + + return Promise.resolve(undefined); + } +} + +export interface HttpLibrary { + send(request: RequestContext): Observable; +} + +export interface PromiseHttpLibrary { + send(request: RequestContext): Promise; +} + +export function wrapHttpLibrary(promiseHttpLibrary: PromiseHttpLibrary): HttpLibrary { + return { + send(request: RequestContext): Observable { + return from(promiseHttpLibrary.send(request)); + } + } +} diff --git a/sdk-template-overrides/typescript/http/isomorphic-fetch.mustache b/sdk-template-overrides/typescript/http/isomorphic-fetch.mustache new file mode 100644 index 00000000..cfb5b154 --- /dev/null +++ b/sdk-template-overrides/typescript/http/isomorphic-fetch.mustache @@ -0,0 +1,56 @@ +import {HttpLibrary, RequestContext, ResponseContext} from './http{{extensionForDeno}}'; +import { from, Observable } from {{#useRxJS}}'rxjs'{{/useRxJS}}{{^useRxJS}}'../rxjsStub{{extensionForDeno}}'{{/useRxJS}}; +import createFetch, { FetchModule } from '@fortaine/fetch'; +{{#platforms}} +{{#browser}} +import "whatwg-fetch"; +{{/browser}} +{{/platforms}} + +{{! https://github.com/ajaishankar/openapi-typescript-fetch#server-side-usage }} +globalThis.fetch = createFetch(fetch as unknown as FetchModule) as any; + +export class IsomorphicFetchHttpLibrary implements HttpLibrary { + + public send(request: RequestContext): Observable { + let method = request.getHttpMethod().toString(); + let body = request.getBody(); + + const resultPromise = fetch(request.getUrl(), { + method: method, + body: body as any, + headers: request.getHeaders(), + {{#platforms}} + {{#browser}} + credentials: "same-origin" + {{/browser}} + {{/platforms}} + }).then((resp: any) => { + const headers: { [name: string]: string } = {}; + resp.headers.forEach((value: string, name: string) => { + headers[name] = value; + }); + + {{#platforms}} + {{#node}} + const body = { + text: () => resp.text(), + binary: () => resp.buffer(), + stream: () => resp.body + }; + {{/node}} + {{^node}} + const body = { + text: () => resp.text(), + binary: () => resp.blob(), + stream: () => resp.body + }; + {{/node}} + {{/platforms}} + return new ResponseContext(resp.status, headers, body); + }); + + return from>(resultPromise); + + } +} diff --git a/sdk-template-overrides/typescript/http/servers.mustache b/sdk-template-overrides/typescript/http/servers.mustache new file mode 100644 index 00000000..5941a247 --- /dev/null +++ b/sdk-template-overrides/typescript/http/servers.mustache @@ -0,0 +1,56 @@ +import { RequestContext, HttpMethod } from "./http/http{{extensionForDeno}}"; +import { Configuration } from "./configuration{{extensionForDeno}}"; + +export interface BaseServerConfiguration { + makeRequestContext(endpoint: string, httpMethod: HttpMethod, configuration: Configuration): RequestContext; +} + +/** + * + * Represents the configuration of a server including its + * url template and variable configuration based on the url. + * + */ +export class ServerConfiguration implements BaseServerConfiguration { + public constructor(private url: string, private variableConfiguration: T) {} + + /** + * Sets the value of the variables of this server. + * + * @param variableConfiguration a partial variable configuration for the variables contained in the url + */ + public setVariables(variableConfiguration: Partial) { + Object.assign(this.variableConfiguration, variableConfiguration); + } + + public getConfiguration(): T { + return this.variableConfiguration + } + + private getUrl() { + let replacedUrl = this.url; + for (const key in this.variableConfiguration) { + var re = new RegExp("{" + key + "}","g"); + replacedUrl = replacedUrl.replace(re, this.variableConfiguration[key]); + } + return replacedUrl + } + + /** + * Creates a new request context for this server using the url with variables + * replaced with their respective values and the endpoint of the request appended. + * + * @param endpoint the endpoint to be queried on the server + * @param httpMethod httpMethod to be used + * + */ + public makeRequestContext(endpoint: string, httpMethod: HttpMethod, configuration: Configuration): RequestContext { + return new RequestContext(this.getUrl() + endpoint, httpMethod, configuration); + } +} + +{{#servers}} +export const server{{-index}} = new ServerConfiguration<{ {{#variables}} "{{name}}": {{#enumValues}}"{{.}}"{{^-last}} | {{/-last}}{{/enumValues}}{{^enumValues}}string{{/enumValues}}{{^-last}},{{/-last}} {{/variables}} }>("{{url}}", { {{#variables}} "{{name}}": "{{defaultValue}}" {{^-last}},{{/-last}}{{/variables}} }) +{{/servers}} + +export const servers = [{{#servers}}server{{-index}}{{^-last}}, {{/-last}}{{/servers}}]; diff --git a/sdk-template-overrides/typescript/logger.mustache b/sdk-template-overrides/typescript/logger.mustache new file mode 100644 index 00000000..ed318b80 --- /dev/null +++ b/sdk-template-overrides/typescript/logger.mustache @@ -0,0 +1,6 @@ +import log from "loglevel"; + +const logger = log.noConflict(); +logger.setLevel((typeof process !== "undefined" && process.env && process.env.DEBUG) ? logger.levels.DEBUG : logger.levels.INFO); + +export { logger }; diff --git a/sdk-template-overrides/typescript/model/ObjectSerializer.mustache b/sdk-template-overrides/typescript/model/ObjectSerializer.mustache new file mode 100644 index 00000000..10508692 --- /dev/null +++ b/sdk-template-overrides/typescript/model/ObjectSerializer.mustache @@ -0,0 +1,356 @@ +{{#models}} +{{#model}} +export * from '{{{ importPath }}}{{extensionForDeno}}'; +{{/model}} +{{/models}} + +{{#models}} +{{#model}} +import { {{classname}}{{#hasEnums}}{{#vars}}{{#isEnum}}, {{classname}}{{enumName}} {{/isEnum}} {{/vars}}{{/hasEnums}} } from '{{{ importPath }}}{{extensionForDeno}}'; +{{/model}} +{{/models}} +import { dateFromRFC3339String, dateToRFC3339String, UnparsedObject } from "../util{{extensionForDeno}}"; +import { logger } from "../logger{{extensionForDeno}}"; + +/* tslint:disable:no-unused-variable */ +const primitives = [ + "string", + "boolean", + "double", + "integer", + "long", + "float", + "number", + "any" + ]; + +const ARRAY_PREFIX = "Array<"; +const MAP_PREFIX = "{ [key: string]: "; +const TUPLE_PREFIX = "["; + +const supportedMediaTypes: { [mediaType: string]: number } = { + "application/json": Infinity, + "application/octet-stream": 0, + "application/x-www-form-urlencoded": 0 +} + + +let enumsMap: Set = new Set([ + {{#models}} + {{#model}} + {{#isEnum}} + "{{classname}}{{enumName}}", + {{/isEnum}} + {{#hasEnums}} + {{#vars}} + {{#isEnum}} + "{{classname}}{{enumName}}", + {{/isEnum}} + {{/vars}} + {{/hasEnums}} + {{/model}} + {{/models}} +]); + +let typeMap: {[index: string]: any} = { + {{#models}} + {{#model}} + {{^isEnum}} + "{{classname}}": {{classname}}, + {{/isEnum}} + {{/model}} + {{/models}} +} + +let oneOfMap: {[index: string]: string[]} = { + {{#models}} + {{#model}} + {{#oneOf}} + {{#-first}} + "{{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}": [{{#oneOf}}{{{#dataType}}}"{{{.}}}"{{^-last}}, {{/-last}}{{{/dataType}}}{{/oneOf}}], + {{/-first}} + {{/oneOf}} + {{/model}} + {{/models}} +}; + +export class ObjectSerializer { + public static findCorrectType(data: any, expectedType: string) { + if (data == undefined) { + return expectedType; + } else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) { + return expectedType; + } else if (expectedType === "Date") { + return expectedType; + } else { + if (enumsMap.has(expectedType)) { + return expectedType; + } + + if (!typeMap[expectedType]) { + return expectedType; // w/e we don't know the type + } + + // Check the discriminator + let discriminatorProperty = typeMap[expectedType].discriminator; + if (discriminatorProperty == null) { + return expectedType; // the type does not have a discriminator. use it. + } else { + if (data[discriminatorProperty]) { + var discriminatorType = data[discriminatorProperty]; + if(typeMap[discriminatorType]){ + return discriminatorType; // use the type given in the discriminator + } else { + return expectedType; // discriminator did not map to a type + } + } else { + return expectedType; // discriminator was not present (or an empty string) + } + } + } + } + + public static serialize(data: any, type: string, format: string) { + if (data == undefined || type == "any") { + return data; + } else if (data instanceof UnparsedObject) { + return data._data; + } else if (primitives.includes(type.toLowerCase()) && typeof data == type.toLowerCase()) { + return data; + } else if (type.startsWith(ARRAY_PREFIX)) { + if (!Array.isArray(data)) { + throw new TypeError(`mismatch types '${data}' and '${type}'`); + } + // Array => Type + const subType: string = type.substring(ARRAY_PREFIX.length, type.length - 1); + const transformedData: any[] = []; + for (const element of data) { + transformedData.push(ObjectSerializer.serialize(element, subType, format)); + } + return transformedData; + } else if (type.startsWith(TUPLE_PREFIX)) { + // We only support homegeneus tuples + const subType: string = type.substring(TUPLE_PREFIX.length, type.length - 1).split(", ")[0]; + const transformedData: any[] = []; + for (const element of data) { + transformedData.push(ObjectSerializer.serialize(element, subType, format)); + } + return transformedData; + } else if (type.startsWith(MAP_PREFIX)) { + // { [key: string]: Type; } => Type + const subType: string = type.substring(MAP_PREFIX.length, type.length - 3); + const transformedData: { [key: string]: any } = {}; + for (const key in data) { + transformedData[key] = ObjectSerializer.serialize(data[key], subType, format); + } + return transformedData; + } else if (type === "Date") { + if ("string" == typeof data) { + return data; + } + if (format == "date" || format == "date-time") { + return dateToRFC3339String(data) + } else { + return data.toISOString(); + } + } else { + if (enumsMap.has(type)) { + return data; + } + if (oneOfMap[type]) { + const oneOfs: any[] = []; + for (const oneOf of oneOfMap[type]) { + try { + oneOfs.push(ObjectSerializer.serialize(data, oneOf, format)); + } catch (e) { + logger.debug(`could not serialize ${oneOf} (${e})`) + } + } + if (oneOfs.length > 1) { + throw new TypeError(`${data} matches multiple types from ${oneOfMap[type]} ${oneOfs}`); + } + if (oneOfs.length == 0) { + throw new TypeError(`${data} doesn't match any type from ${oneOfMap[type]} ${oneOfs}`); + } + return oneOfs[0]; + } + + if (!typeMap[type]) { // in case we dont know the type + return data; + } + + // Get the actual type of this object + type = this.findCorrectType(data, type); + + // get the map for the correct type. + let attributeTypes = typeMap[type].getAttributeTypeMap(); + let instance: {[index: string]: any} = {}; + for (let index in attributeTypes) { + let attributeType = attributeTypes[index]; + instance[attributeType.baseName] = ObjectSerializer.serialize(data[attributeType.baseName], attributeType.type, attributeType.format); + } + return instance; + } + } + + public static deserialize(data: any, type: string, format: string) { + // polymorphism may change the actual type. + type = ObjectSerializer.findCorrectType(data, type); + if (data == undefined || type == "any") { + return data; + } else if (primitives.includes(type.toLowerCase()) && typeof data == type.toLowerCase()) { + return data; + } else if (type.startsWith(ARRAY_PREFIX)) { + // Assert the passed data is Array type + if (!Array.isArray(data)) { + throw new TypeError(`mismatch types '${data}' and '${type}'`); + } + // Array => Type + const subType: string = type.substring(ARRAY_PREFIX.length, type.length - 1); + const transformedData: any[] = []; + for (const element of data) { + transformedData.push(ObjectSerializer.deserialize(element, subType, format)); + } + return transformedData; + } else if (type.startsWith(TUPLE_PREFIX)) { + // [Type,...] => Type + const subType: string = type.substring(TUPLE_PREFIX.length, type.length - 1).split(", ")[0]; + const transformedData: any[] = []; + for (const element of data) { + transformedData.push(ObjectSerializer.deserialize(element, subType, format)); + } + return transformedData; + } else if (type.startsWith(MAP_PREFIX)) { + // { [key: string]: Type; } => Type + const subType: string = type.substring(MAP_PREFIX.length, type.length - 3); + const transformedData: { [key: string]: any } = {}; + for (const key in data) { + transformedData[key] = ObjectSerializer.deserialize(data[key], subType, format); + } + return transformedData; + } else if (type === "Date") { + return dateFromRFC3339String(data) + } else { + if (enumsMap.has(type)) {// is Enum + return data; + } + if (oneOfMap[type]) { + const oneOfs: any[] = []; + for (const oneOf of oneOfMap[type]) { + try { + const d = ObjectSerializer.deserialize(data, oneOf, format); + if (!d?._unparsed) { + oneOfs.push(d); + } + } catch (e) { + logger.debug(`could not deserialize ${oneOf} (${e})`) + } + + } + if (oneOfs.length != 1) { + return new UnparsedObject(data); + } + return oneOfs[0]; + } + + if (!typeMap[type]) { // dont know the type + return data; + } + let instance = new typeMap[type](); + let attributeTypes = typeMap[type].getAttributeTypeMap(); + for (let index in attributeTypes) { + let attributeType = attributeTypes[index]; + let value = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type, attributeType.format); + if (value !== undefined) { + instance[attributeType.name] = value; + } + } + return instance; + } + } + + + /** + * Normalize media type + * + * We currently do not handle any media types attributes, i.e. anything + * after a semicolon. All content is assumed to be UTF-8 compatible. + */ + public static normalizeMediaType(mediaType: string | undefined): string | undefined { + if (mediaType === undefined) { + return undefined; + } + return mediaType.split(";")[0].trim().toLowerCase(); + } + + /** + * From a list of possible media types, choose the one we can handle best. + * + * The order of the given media types does not have any impact on the choice + * made. + */ + public static getPreferredMediaType(mediaTypes: Array): string { + /** According to OAS 3 we should default to json */ + if (!mediaTypes) { + return "application/json"; + } + + const normalMediaTypes = mediaTypes.map(this.normalizeMediaType); + let selectedMediaType: string | undefined = undefined; + let selectedRank: number = -Infinity; + for (const mediaType of normalMediaTypes) { + if (supportedMediaTypes[mediaType!] > selectedRank) { + selectedMediaType = mediaType; + selectedRank = supportedMediaTypes[mediaType!]; + } + } + + if (selectedMediaType === undefined) { + throw new Error("None of the given media types are supported: " + mediaTypes.join(", ")); + } + + return selectedMediaType!; + } + + /** + * Convert data to a string according the given media type + */ + public static stringify(data: any, mediaType: string): string { + if (mediaType === "text/plain") { + return String(data); + } + + if (mediaType === "application/json") { + return JSON.stringify(data); + } + + throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.stringify."); + } + + /** + * Parse data from a string according to the given media type + */ + public static parse(rawData: string, mediaType: string | undefined) { + if (mediaType === undefined) { + throw new Error("Cannot parse content. No Content-Type defined."); + } + + if (mediaType === "text/plain") { + return rawData; + } + + if (mediaType === "application/json") { + return JSON.parse(rawData); + } + + if (mediaType === "text/html") { + return rawData; + } + + if (mediaType === "text/event-stream") { + return rawData; + } + + throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.parse."); + } +} diff --git a/sdk-template-overrides/typescript/model/model.mustache b/sdk-template-overrides/typescript/model/model.mustache new file mode 100644 index 00000000..99cce201 --- /dev/null +++ b/sdk-template-overrides/typescript/model/model.mustache @@ -0,0 +1,84 @@ +{{>licenseInfo}} +{{#models}} +{{#model}} +{{#tsImports}} +import { {{classname}} } from '{{filename}}{{extensionForDeno}}'; +{{/tsImports}} +import { HttpFile } from '../http/http{{extensionForDeno}}'; + +{{#description}} +/** +* {{{.}}} +*/ +{{/description}} +{{^isEnum}} +export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ +{{#vars}} +{{#description}} + /** + * {{{.}}} + */ +{{/description}} + '{{baseName}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}; +{{/vars}} + + {{#discriminator}} + static readonly discriminator: string | undefined = "{{discriminatorName}}"; + {{/discriminator}} + {{^discriminator}} + static readonly discriminator: string | undefined = undefined; + {{/discriminator}} + + {{^isArray}} + static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [ + {{#vars}} + { + "name": "{{name}}", + "baseName": "{{baseName}}", + "type": "{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}", + "format": "{{dataFormat}}" + }{{^-last}}, + {{/-last}} + {{/vars}} + ]; + + static getAttributeTypeMap() { + {{#parent}} + return super.getAttributeTypeMap().concat({{classname}}.attributeTypeMap); + {{/parent}} + {{^parent}} + return {{classname}}.attributeTypeMap; + {{/parent}} + } + {{/isArray}} + + public constructor() { + {{#parent}} + super(); + {{/parent}} + {{#allVars}} + {{#discriminatorValue}} + this.{{name}} = "{{discriminatorValue}}"; + {{/discriminatorValue}} + {{/allVars}} + {{#discriminatorName}} + this.{{discriminatorName}} = "{{classname}}"; + {{/discriminatorName}} + } +} + +{{#hasEnums}} + +{{#vars}} +{{#isEnum}} +export type {{classname}}{{enumName}} ={{#allowableValues}}{{#values}} "{{.}}" {{^-last}}|{{/-last}}{{/values}}{{/allowableValues}}; +{{/isEnum}} +{{/vars}} + +{{/hasEnums}} +{{/isEnum}} +{{#isEnum}} +export type {{classname}} ={{#allowableValues}}{{#values}} "{{.}}" {{^-last}}|{{/-last}}{{/values}}{{/allowableValues}}; +{{/isEnum}} +{{/model}} +{{/models}} \ No newline at end of file diff --git a/sdk-template-overrides/typescript/package.mustache b/sdk-template-overrides/typescript/package.mustache new file mode 100644 index 00000000..775cc21e --- /dev/null +++ b/sdk-template-overrides/typescript/package.mustache @@ -0,0 +1,87 @@ +{ + "name": "{{npmName}}", + "version": "{{npmVersion}}", + "description": "OpenAPI client for {{npmName}}", + "author": "OpenAPI-Generator Contributors", + "repository": { + "type": "git", + "url": "https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}.git" + }, + "keywords": [ + "fetch", + "typescript", + "openapi-client", + "openapi-generator" + ], + "license": "Unlicense", + "type": "module", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "typings": "./dist/index.d.ts", + "types": "dist/index.d.ts", + "scripts": { + "clean": "rimraf ./dist", + "prebuild": "npm run clean", + "build": "tsc -p .", + "prepare": "npm run build", + "prepublishOnly": "npm run prepare" + }, + "dependencies": { + "@fortaine/fetch": "^6.2.2", + {{#frameworks}} + {{#fetch-api}} + {{#platforms}} + {{#browser}} + "whatwg-fetch": "^3.0.0", + {{/browser}} + {{/platforms}} + {{/fetch-api}} + {{#jquery}} + "jquery": "^3.4.1", + {{/jquery}} + {{/frameworks}} + {{#platforms}} + {{#node}} + "btoa": "^1.2.1", + "form-data": "^4.0.0", + {{/node}} + {{/platforms}} + {{#useInversify}} + "inversify": "^5.0.1", + {{/useInversify}} + {{#useRxJS}} + "rxjs": "^6.4.0", + {{/useRxJS}} + "loglevel": "^1.8.1", + "filedirname": "^2.7.0", + "es6-promise": "^4.2.4", + "url-parse": "^1.4.3" + }, + "devDependencies": { + {{#frameworks}} + {{#jquery}} + "@types/jquery": "^3.3.29", + {{/jquery}} + {{/frameworks}} + {{#platforms}} + {{#node}} + "@types/node": "*", + {{/node}} + {{/platforms}} + "@types/url-parse": "1.4.4", + "rimraf": "^3.0.2", + "tsconfig-to-dual-package": "^1.1.0", + "typescript": "^4.0" + }{{#npmRepository}},{{/npmRepository}} +{{#npmRepository}} + "publishConfig":{ + "registry":"{{npmRepository}}" + } +{{/npmRepository}} +} diff --git a/sdk-template-overrides/typescript/tsconfig.mustache b/sdk-template-overrides/typescript/tsconfig.mustache new file mode 100644 index 00000000..e59b759b --- /dev/null +++ b/sdk-template-overrides/typescript/tsconfig.mustache @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "strict": true, + /* Basic Options */ + {{#supportsES6}} + "target": "es2022", + "module": "esnext", + "esModuleInterop": true, + {{/supportsES6}} + {{^supportsES6}} + "target": "es5", + {{/supportsES6}} + "moduleResolution": "node", + "declaration": true, + + /* Additional Checks */ + "noUnusedLocals": false, /* Report errors on unused locals. */ // TODO: reenable (unused imports!) + "noUnusedParameters": false, /* Report errors on unused parameters. */ // TODO: set to true again + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + "removeComments": true, + "sourceMap": true, + "outDir": "./dist", + "noLib": false, + {{#platforms}} + {{#node}} + "lib": [ "es2022", "dom" ], /* https://github.com/microsoft/TypeScript/issues/43990#issuecomment-834577871 */ + {{/node}} + {{#browser}} + "lib": [ "es6", "dom" ], + {{/browser}} + {{/platforms}} + {{#useInversify}} + "experimentalDecorators": true, + {{/useInversify}} + }, + "exclude": [ + "dist", + "node_modules" + ], + "filesGlob": [ + "./**/*.ts", + ] +} diff --git a/sdk-template-overrides/typescript/util.mustache b/sdk-template-overrides/typescript/util.mustache new file mode 100644 index 00000000..dafce9fe --- /dev/null +++ b/sdk-template-overrides/typescript/util.mustache @@ -0,0 +1,135 @@ +/** + * Returns if a specific http code is in a given code range + * where the code range is defined as a combination of digits + * and "X" (the letter X) with a length of 3 + * + * @param codeRange string with length 3 consisting of digits and "X" (the letter X) + * @param code the http status code to be checked against the code range + */ +export function isCodeInRange(codeRange: string, code: number): boolean { + // This is how the default value is encoded in OAG + if (codeRange === "0") { + return true; + } + if (codeRange == code.toString()) { + return true; + } else { + const codeString = code.toString(); + if (codeString.length != codeRange.length) { + return false; + } + for (let i = 0; i < codeString.length; i++) { + if (codeRange.charAt(i) != "X" && codeRange.charAt(i) != codeString.charAt(i)) { + return false; + } + } + return true; + } +} + +/** +* Returns if it can consume form +* +* @param consumes array +*/ +export function canConsumeForm(contentTypes: string[]): boolean { + return contentTypes.indexOf('multipart/form-data') !== -1 +} + +export class UnparsedObject { + _data:any; + constructor(data:any) { + this._data = data; + } +} + +export type AttributeTypeMap = { + [key: string]: { + baseName: string; + type: string; + required?: boolean; + format?: string; + }; +} + +{{#platforms}} +{{#browser}} +export const isBrowser: boolean = typeof window !== "undefined" && typeof window.document !== "undefined"; +{{/browser}} +{{#node}} +export const isNode: boolean = typeof process !== "undefined" && process.release && process.release.name === 'node'; +{{/node}} +{{/platforms}} + +export class DDate extends Date { + rfc3339TzOffset: string | undefined; +} + +const RFC3339Re: RegExp = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})\.?(\d+)?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/; +export function dateFromRFC3339String(date: string): DDate { + const m = RFC3339Re.exec(date); + if (m) { + const _date = new DDate(date) + if( m[8] === undefined && m[9] === undefined){ + _date.rfc3339TzOffset = 'Z' + } else { + _date.rfc3339TzOffset = `${m[8]}:${m[9]}` + } + + return _date + } else { + throw new Error('unexpected date format: ' + date) + } +} + +export function dateToRFC3339String(date: Date | DDate): string { + const offSetArr = getRFC3339TimezoneOffset(date).split(":") + const tzHour = offSetArr.length == 1 ? 0 : +offSetArr[0]; + const tzMin = offSetArr.length == 1 ? 0 : +offSetArr[1]; + + const year = date.getFullYear() ; + const month = date.getMonth(); + const day = date.getUTCDate(); + const hour = date.getUTCHours() + +tzHour; + const minute = date.getUTCMinutes() + +tzMin; + const second = date.getUTCSeconds(); + + let msec = date.getUTCMilliseconds().toString(); + msec = +msec === 0 ? "" : `.${pad(+msec, 3)}` + + return year + "-" + + pad(month + 1) + "-" + + pad(day) + "T" + + pad(hour) + ":" + + pad(minute) + ":" + + pad(second) + + msec + + offSetArr.join(":"); +} + +// Helpers +function pad(num: number, len: number = 2): string { + let paddedNum = num.toString() + if (paddedNum.length < len) { + paddedNum = "0".repeat(len - paddedNum.length) + paddedNum + } else if (paddedNum.length > len) { + paddedNum = paddedNum.slice(0, len) + } + + return paddedNum +} + +function getRFC3339TimezoneOffset(date: Date | DDate): string { + if (date instanceof DDate && date.rfc3339TzOffset) { + return date.rfc3339TzOffset; + } + + let offset = date.getTimezoneOffset() + if (offset === 0) { + return "Z"; + } + + const offsetSign = (offset > 0) ? "+" : "-"; + offset = Math.abs(offset); + return offsetSign + pad(Math.floor(offset / 60)) + ":" + pad(offset % 60); +}