jswagger-client
Version:
This is jswagger's client package.
374 lines (337 loc) • 14.9 kB
text/typescript
import axios, {
AxiosError, AxiosRequestConfig, AxiosResponse, AxiosTransformer
} from 'axios';
import JSONbig from 'node-json-bigint';
import * as url from 'url';
import * as querystring from 'querystring';
import {
ApiError, ApiRequestOptions, ApiRequestOptionsOO, IApiMetadata,
IApiSecurityContext,
IRewriterParams,
ISwaggerApiOptions,
ISwaggerClientConfig, QueryNameValue
} from './types';
import {
findApisByTag
} from 'jswagger-common';
import {
arrayBufferToBuffer,
leafConvertToClassValue
} from './internals';
function extractRef(ref: string): {
type: string,
name: string
} | null {
const refMatcher = /#\/(\w+)\/(.*)/.exec(ref);
if (!refMatcher) {
return null;
}
return {
type: refMatcher[1],
name: refMatcher[2]
};
}
const REGEX_CONTENT_TYPE_APPLICATION_JSON = /application\/json(?:\s*;\s*charset=([a-zA-Z0-9_-]+))?/;
const REGEX_CONTENT_TYPE_TEXT = /text\/([^;]+)(?:\s*;\s*charset=([a-zA-Z0-9_-]+))?/;
function toBufferEncoding(input: string | undefined) {
return (input && input.toUpperCase().startsWith('ISO-8859') && 'ascii') || input || 'utf8';
}
function basicTransformResponse(data: any, headers: any): any {
const jsonCheck = headers['content-type'] && REGEX_CONTENT_TYPE_APPLICATION_JSON.exec(headers['content-type']);
const textCheck = jsonCheck ? null : headers['content-type'] && REGEX_CONTENT_TYPE_TEXT.exec(headers['content-type']);
const dataBuffer: Buffer = (Buffer.isBuffer(data) ? data : arrayBufferToBuffer(data));
const encoding = toBufferEncoding(jsonCheck ? jsonCheck[1] : textCheck ? textCheck[2] : undefined);
if (jsonCheck) {
const text = dataBuffer.toString(encoding);
return JSONbig.parse(text);
} else if (textCheck) {
return dataBuffer.toString(encoding);
}
return dataBuffer;
}
function urlConcat(a: string, b: string): string {
if (a.length && b.length) {
const ta = a.charAt(a.length - 1) === '/';
const tb = b.charAt(0) === '/';
if (ta === tb) {
if (ta) {
return a.concat(b.substring(1));
} else {
return a.concat('/', b);
}
}
}
return a.concat(b);
}
function concatHandlers<T>(first: T, others?: T | T[]): T[] {
if (others && others) {
if (Array.isArray(others)) {
return [first].concat(others);
} else {
return [first, others];
}
} else {
return [first];
}
}
function safePromiseCallback<T>(runner: () => Promise<T>): Promise<T> {
try {
return runner();
} catch (e) {
return Promise.reject(e);
}
}
const CONTENT_TYPE_TESTERS = [
(data) => Buffer.isBuffer(data) ? 'application/octet-stream' : null,
(data) => (typeof data === 'object') ? 'application/json;charset=utf-8' : null,
(data) => (typeof data === 'string') ? 'text/plain' : null
];
export default class SwaggerClient {
private readonly _config: ISwaggerClientConfig;
private readonly _baseUrl: string;
public static urlConcat(a: string, b: string): string {
return urlConcat(a, b);
}
constructor(config: ISwaggerClientConfig) {
this._config = config;
if (config.baseUrl) {
this._baseUrl = config.baseUrl;
} else {
this._baseUrl = url.format({
protocol: config.protocol || config.spec.protocol || 'http',
host: config.host || config.spec.host,
pathname: config.spec.basePath
});
}
}
private _defaultContentTypeResolve(params: IRewriterParams, data: any): string {
const r = CONTENT_TYPE_TESTERS.reduce<string | null>((prev, cur) => {
if (prev) {
return prev;
} else {
return cur(data);
}
}, null);
return r || 'text/plain';
}
public api<TAPI>(api: IApiMetadata, options?: ISwaggerApiOptions): TAPI {
const _options: ISwaggerApiOptions = options || {};
const self = this;
const apis = findApisByTag(this._config.spec, api.tag);
const proxy: TAPI = {} as TAPI;
const definitionClasses: Map<string, any> =
api.specMetadata.classes.reduce<Map<string, any>>(
(results, clazz) => {
const definitionName = api.specMetadata.getSwaggerDefinitionName(clazz);
results.set(definitionName, clazz);
return results;
}, new Map<string, any>()
);
apis.forEach(item => {
let apiPath = item.path;
Object.defineProperty(proxy, item.api.operationId, {
get: () => function() {
let callOptions: undefined | ApiRequestOptionsOO<any, any> = arguments[0];
let retryCount = 0;
const retryParams: IRewriterParams = {
operationId: item.api.operationId,
arg: callOptions
};
return (
self._config.contentTypeResolver && self._config.contentTypeResolver(retryParams, callOptions && callOptions.data) ||
Promise.resolve(null)
).then(contentType =>
contentType || self._defaultContentTypeResolve(retryParams, callOptions && callOptions.data)
).then(contentType => {
const doExecute = () => {
if (self._config.argRewriter) {
const replaced = self._config.argRewriter({
operationId: item.api.operationId,
arg: callOptions && Object.assign({}, callOptions)
});
callOptions = replaced || callOptions;
}
const apiRequestOptions: ApiRequestOptions | undefined =
callOptions && (callOptions as ApiRequestOptions);
const securityContext: IApiSecurityContext | undefined =
_options['securityContext'] || self._config.securityContext;
const optBody: undefined | any = callOptions && callOptions['data'];
const optParams: { [key: string]: any } | undefined = callOptions && callOptions['params'];
const reqBody = optBody;
const baseUrl = apiRequestOptions && apiRequestOptions.baseURL || self._baseUrl;
const rewriterParams: IRewriterParams = {
operationId: item.api.operationId,
arg: callOptions
};
let reqHeaders: Record<string, string> = {
'content-type': contentType
};
let reqQueries = {};
if (optParams) {
item.api.parameters.forEach(parameterInfo => {
if (parameterInfo.in === 'header') {
const foundItem = Object.entries(optParams).find(([key, value]) => key === parameterInfo.name);
if (foundItem) {
reqHeaders[parameterInfo.name] = foundItem[1];
}
} else if (parameterInfo.in === 'query') {
const foundItem = Object.entries(optParams).find(([key, value]) => key === parameterInfo.name);
if (foundItem) {
reqQueries[parameterInfo.name] = foundItem[1];
}
} else if (parameterInfo.in === 'path') {
const foundItem = Object.entries(optParams).find(([key, value]) => key === parameterInfo.name);
if (foundItem) {
apiPath = apiPath.replace(`{${parameterInfo.name}}`, foundItem[1]);
}
}
});
}
let apiUrl = new url.URL(urlConcat(baseUrl, apiPath));
if (apiRequestOptions && apiRequestOptions.protocol) {
apiUrl.protocol = apiRequestOptions.protocol;
}
if (apiRequestOptions && apiRequestOptions.host) {
apiUrl.host = apiRequestOptions.host;
}
if (self._config.hostRewriter) {
const result = self._config.hostRewriter(rewriterParams);
if (result) {
apiUrl.protocol = result.protocol || apiUrl.protocol;
apiUrl.host = result.host || apiUrl.host;
}
}
if (self._config.urlRewriter) {
const result = self._config.urlRewriter(rewriterParams, apiUrl);
apiUrl = result || apiUrl;
}
let searchParams: QueryNameValue[] = [];
Object.keys(reqQueries)
.forEach(k => {
searchParams.push({
name: k,
value: reqQueries[k]
});
});
if (apiRequestOptions && apiRequestOptions.queries) {
searchParams.push(...apiRequestOptions.queries);
}
if (securityContext) {
if (securityContext.headerReplacer) {
reqHeaders = securityContext.headerReplacer(reqHeaders);
}
if (securityContext.queryReplacer) {
searchParams = securityContext.queryReplacer(searchParams);
}
}
if (apiRequestOptions && apiRequestOptions.headers) {
Object.assign(reqHeaders, apiRequestOptions.headers);
}
searchParams.forEach(item => {
apiUrl.searchParams.append(item.name, item.value);
});
const axioxRequestConfig = Object.assign({}, apiRequestOptions || {});
if (axioxRequestConfig['securityContext']) delete axioxRequestConfig['securityContext'];
if (axioxRequestConfig['params']) delete axioxRequestConfig['params'];
if (axioxRequestConfig['queries']) delete axioxRequestConfig['queries'];
if (axioxRequestConfig['data']) delete axioxRequestConfig['data'];
if (axioxRequestConfig['baseURL']) delete axioxRequestConfig['baseURL'];
Object.assign(
axioxRequestConfig,
{
responseType: 'arraybuffer',
headers: reqHeaders,
httpAgent: self._config.httpAgent,
httpsAgent: self._config.httpsAgent,
transformResponse: concatHandlers<AxiosTransformer>(
basicTransformResponse,
apiRequestOptions && apiRequestOptions.transformResponse
)
}
);
return ((() => {
if (['get', 'delete', 'head', 'options'].includes(item.method)) {
type CallType = (url: string, config: AxiosRequestConfig) => Promise<AxiosResponse>;
return (axios[item.method] as CallType)(apiUrl.toString(), axioxRequestConfig);
} else {
type CallType = (url: string, data: any, config: AxiosRequestConfig) => Promise<AxiosResponse>;
return (axios[item.method] as CallType)(apiUrl.toString(), reqBody, axioxRequestConfig);
}
})())
.then(res => {
const responseDefinition = item.api.responses && item.api.responses[res.status.toString()];
const responseRef = responseDefinition && responseDefinition.schema && extractRef(responseDefinition.schema.$ref);
const responseClazz = responseRef && definitionClasses.get(responseRef.name);
const out = Object.assign({}, res);
if (responseClazz) {
out.data = new responseClazz({
schema: res.data
});
} else if (responseDefinition && responseDefinition.schema) {
out.data = leafConvertToClassValue(responseDefinition.schema as any, res.data);
}
return out;
})
.catch((err: AxiosError) => {
if (err.response) {
const responseDefinition = item.api.responses && item.api.responses[err.response.status.toString()];
const responseRef = responseDefinition && responseDefinition.schema && responseDefinition.schema.$ref;
const responseDefinitionClass = (() => {
const refMatcher = extractRef(responseRef);
if (!refMatcher) {
return undefined;
}
return api.specMetadata.classes
.find(v => api.specMetadata.getSwaggerDefinitionName(v) === refMatcher.name);
})();
const responseVO = responseDefinitionClass ?
new responseDefinitionClass({
schema: err.response.data
}) : err.response.data;
return Promise.reject(new ApiError({
message: responseDefinition ? responseDefinition.description || err.message : err.message,
code: err.code || 'ApiError',
status: err.response.status,
data: responseVO,
headers: err.response.headers,
axiosError: err,
axiosConfig: err.config,
axiosRequest: err.request,
axiosResponse: err.response
}));
} else {
return Promise.reject(err);
}
});
};
const doExecuteWithRetry = (resolve, reject) => {
doExecute()
.then(resolve)
.catch(e => {
if (self._config.retryHandler) {
safePromiseCallback(self._config.retryHandler.bind(null, retryParams, retryCount++, e))
.then(delay => {
if (delay < 0 || delay === false) {
reject(e);
} else if (delay === 0) {
doExecuteWithRetry(resolve, reject);
} else {
setTimeout(() => doExecuteWithRetry(resolve, reject), delay);
}
})
.catch(reject);
} else {
reject(e);
}
});
};
return new Promise<AxiosResponse>((resolve, reject) => {
doExecuteWithRetry(resolve, reject);
});
});
}
});
});
return proxy;
}
}