api-first-spec
Version:
Describe API specification as a test
389 lines (363 loc) • 12.8 kB
text/typescript
import querystring from "querystring";
import Request = require("request");
import { API } from "./api";
export interface IDefaultParameters {
headers?: { [key: string]: string};
params?: { [key: string]: string};
}
interface IRequestParams {
method: string;
headers?: { [key: string]: string};
path: string;
data: any;
log: boolean;
}
interface IHttpResult {
data: any;
res: Request.Response;
req: Request.Request;
params?: any;
}
/**
* Wrapper of http/https library.
*
* `defaults` expect have two keys: params and headers
*
* @param {string} host
* @param {boolean} ssl use to switch using `https` and `http`
* @param {object} defaults will set unmodifiable default value
* @return {object}
*/
export class HttpClient {
private request: Request.RequestAPI<Request.Request, Request.CoreOptions, Request.RequiredUriUrl>;
private cookieJar: Request.CookieJar;
private _api: API = null;
private _params: any = null;
private _headers: any = null;
constructor(private config: HttpClientConfig, private defaults: IDefaultParameters = {}) {
this.cookieJar = Request.jar();
this.request = Request.defaults({ jar: this.cookieJar});
if (!defaults.headers) {
defaults.headers = {};
}
if (!defaults.params) {
defaults.params = {};
}
}
public api(v: API) {
this._api = v;
return this;
}
public params(v: any) {
this._params = v;
return this;
}
public headers(v: any) {
this._headers = v;
return this;
}
public success(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isSuccess(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't Success.\n${this.stringify(result.data)}`);
}
this._api.validateResponse(result.data, result.res, result.params);
return this.done(callback, result);
}, validateInput);
}
public badRequest(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isBadRequest(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't BadRequest.\n${this.stringify(result.data)}`);
}
return this.done(callback, result);
}, validateInput);
}
public notFound(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isNotFound(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't NotFound.\n${this.stringify(result.data)}`);
}
return this.done(callback, result);
}, validateInput);
}
public unauthorized(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isUnauthorized(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't Unauthorized.\n${this.stringify(result.data)}`);
}
return this.done(callback, result);
}, validateInput);
}
public forbidden(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isForbidden(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't Forbidden.\n${this.stringify(result.data)}`);
}
return this.done(callback, result);
}, validateInput);
}
public clientError(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, validateInput: boolean = this.config.validateRequest) {
return this.doTest(result => {
if (!this._api.isClientError(result.data, result.res)) {
throw new Error(`StatusCode=${result.res.statusCode}. Response isn't ClientError.\n${this.stringify(result.data)}`);
}
return this.done(callback, result);
}, validateInput);
}
public execute(): Promise<IHttpResult> {
return this.doTest(null, false);
}
public setDefaultHeaders(headers: { [key: string]: string}) {
this.defaults.headers = headers;
}
public setDefaultParams(params: { [key: string]: string}) {
this.defaults.params = params;
}
private normalizeData(params: any): any {
function getValue(v: any) {
if (typeof(v) === "function") {
v = v(params);
}
return querystring.escape(v);
}
if (typeof(params) === "function") {
params = params();
}
const ret: any = {};
for (let key in params) {
if (params.hasOwnProperty(key)) {
let value = params[key];
if (typeof(value) === "function") {
value = value(params);
}
if (key.endsWith("[]")) {
key = key.substring(0, key.length - 2);
}
ret[key] = value;
}
}
return ret;
}
private toJson(params: any) {
function normalize(p: any) {
let type = typeof(p);
let ret = p;
if (type === "function") {
p = p();
type = typeof(p);
}
if (type === "object") {
if (Array.isArray(p)) {
ret = [];
for (const item of p) {
ret.push(normalize(item));
}
} else {
ret = {};
for (const key in p) {
if (p.hasOwnProperty(key)) {
ret[key] = normalize(p[key]);
}
}
}
}
return ret;
}
if (typeof(params) === "function") {
params = params();
}
params = normalize(params);
return JSON.stringify(params);
}
private done(callback: (data?: any, res?: Request.Response, req?: Request.Request) => any, result: IHttpResult) {
function isDone() {
return callback.toString().indexOf("done() invoked with non-Error") !== -1;
}
if (!callback) {
return;
}
if (isDone()) {
return callback();
} else {
return callback(result.data, result.res, result.req);
}
}
private stringify(data: any) {
if (typeof(data) === "object") {
return JSON.stringify(data);
}
return "" + data;
}
private doTest(callback: (ret: IHttpResult) => void, validateInput: boolean = this.config.validateRequest): Promise<any> {
const self = this;
return new Promise((resolve, reject) => {
function isJson(contentType: string) {
if (!contentType) return false;
const ct = contentType.toLowerCase();
return ct.indexOf("/json") > 0 || ct.indexOf("+json") > 0;
}
function buildEndpoint() {
let ret = currentApi.endpoint;
if (currentApi.urlParams) {
currentApi.urlParams.forEach((key: string) => {
ret = ret.replace("[" + key + "]", currentParams[key]);
delete currentParams[key];
});
}
return (self.config.ssl ? "https" : "http") + "://" + self.config.host + ret;
}
function doCallback(ret: IHttpResult) {
if (currentApi.verbose()) {
/* tslint:disable:no-console */
console.log("**** Response Data ****");
console.log("statusCode = " + ret.res.statusCode);
console.log("data = ");
console.log(ret.data);
}
if (!callback) {
resolve(ret);
return;
}
const ct = ret.res.headers["content-type"];
if (isJson(ct)) {
ret.data = JSON.parse(ret.data);
}
const ret2 = callback(ret);
resolve(ret2 ? ret2 : ret.data);
}
function getParamsForValidation(): any {
const result = Object.assign({}, currentParams);
if (currentApi.urlParams) {
currentApi.urlParams.forEach(key => {
delete result[key];
});
}
return normalize(result);
}
// Expand parameters which contains `.` for GET. e.g. `sortOrder.column`
function normalize(params: any): any {
if (!params || typeof(params) !== "object") {
return params;
}
const newResult: any = {};
Object.keys(params).filter(key => key.indexOf(".") === -1).forEach(key => {
newResult[key] = params[key];
});
Object.keys(params).filter(key => key.indexOf(".") !== -1).forEach(key => {
const array = key.split(".");
const parentKey = array.shift();
const childKey = array.join(".");
if (newResult[parentKey]) {
newResult[parentKey][childKey] = params[key];
} else {
const newChild = {
[childKey]: params[key]
};
newResult[parentKey] = newChild;
}
});
Object.keys(newResult).filter(key => typeof(newResult[key]) === "object" && !Array.isArray(newResult[key])).forEach(key => {
newResult[key] = normalize(newResult[key]);
});
return newResult;
}
const currentApi = self._api;
const currentParams = Object.assign({}, self.defaults.params, self._params);
const currentHeaders = Object.assign({}, self.defaults.headers, self._headers);
try {
self._params = null;
self._headers = null;
if (!currentApi) {
throw new Error("api isn't set.");
}
if (validateInput && currentApi.request.params && currentApi.request.params.hasChildren()) {
const paramsForValidation = getParamsForValidation();
currentApi.request.params.validate(paramsForValidation, paramsForValidation, null);
}
const requestParams: IRequestParams = {
method: currentApi.method,
headers: currentHeaders,
path: buildEndpoint(),
data: Object.keys(currentParams).length > 0 ? currentParams : null,
log: currentApi.verbose() as boolean
};
if (currentApi.request && currentApi.request.headers) {
Object.assign(requestParams.headers, currentApi.request.headers);
}
if (currentApi.request && currentApi.request.contentType) {
requestParams.headers["Content-Type"] = currentApi.request.contentType;
}
self.doRequest(requestParams, doCallback);
} catch (e) {
setTimeout(() => {
throw e;
}, 0);
}
});
}
private doRequest(config: IRequestParams, callback: (ret: IHttpResult) => void) {
const requestOptions: any = Object.assign({
method: config.method,
headers: config.headers || {},
url: config.path
}, this.config.requestOptions || {});
if (config.method === "GET" && config.data) {
requestOptions.qs = config.data;
} else if (config.data) {
let ct = config.headers["Content-Type"];
if (!ct) {
ct = "application/x-www-form-urlencoded";
requestOptions.headers["Content-Type"] = ct;
}
if (ct === "application/x-www-form-urlencoded") {
requestOptions.form = this.normalizeData(config.data);
} else if (ct === "multipart/form-data") {
const formData: any = {};
Object.keys(config.data).forEach(key => {
const value = config.data[key];
if (Array.isArray(value)) {
formData[key + "[]"] = value;
} else {
formData[key] = value;
}
});
requestOptions.formData = formData;
} else if (ct === "application/json") {
requestOptions.body = this.toJson(config.data);
}
}
if (config.log) {
console.log("**** Request Data ****");
console.log(`${config.method} ${config.path}`);
if (config.data) {
console.log(JSON.stringify(config.data));
}
}
const req = this.request(requestOptions, (error: Error, res: Request.Response, body: any) => {
if (error) {
console.log("!!!!! error !!!!!", error, res, body);
return;
}
const result: IHttpResult = {
data: body,
res,
req,
params: config.data
};
callback(result);
});
}
private copy() {
const ret = new HttpClient(this.config, this.defaults);
ret.assign(this.cookieJar, this._api, this._params, this._headers);
return ret;
}
private assign(cookieJar: Request.CookieJar, api: API, params: any, headers: any) {
this.cookieJar = cookieJar;
this._api = api;
this._params = params;
this._headers = headers;
this.request = this.request.defaults({ jar: cookieJar});
}
}