api-first-spec
Version:
Describe API specification as a test
211 lines (193 loc) • 6.28 kB
text/typescript
import { IParam, Param, TopLevelParam } from "./param";
import { ParamPool } from "./paramPool";
import request from "request";
export enum Method {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
export enum ContentType {
CSV = "text/csv",
TEXT = "text/plain",
JSON = "application/json",
URLENCODED = "application/x-www-form-urlencoded",
MULTIPART = "multipart/form-data"
}
export class Request {
private _contentType: string;
private _params?: IParam = null;
private _rules?: any = null;
private _headers: any = null;
constructor(config: RequestConfig) {
this._contentType = config.contentType ? config.contentType : ContentType.URLENCODED;
this._rules = config.rules;
this._headers = config.headers;
if (config.params) {
this._params = new TopLevelParam("request", config.params, config.rules);
}
}
public get contentType() { return this._contentType; }
public get params() { return this._params; }
public get rules() { return this._rules; }
public get headers() { return this._headers; }
}
export class Response {
private _contentType: string;
private _data?: IParam = null;
private _rules?: any = null;
constructor(config: ResponseConfig) {
this._contentType = config.contentType ? config.contentType : ContentType.JSON;
this._rules = config.rules;
if (config.data) {
this._data = new TopLevelParam("response", config.data, config.rules);
}
}
public get contentType() { return this._contentType; }
public get data() { return this._data; }
public get rules() { return this._rules; }
}
export class API {
private _name: string;
private _endpoint: string;
private _method: string;
private _request: Request;
private _response: Response;
private _urlParams: Array<string>;
private _verbose: boolean = false;
constructor(config: ApiConfig) {
this.init(config);
}
public get name() { return this._name; }
public get endpoint() { return this._endpoint; }
public get method() { return this._method; }
public get request() { return this._request; }
public get response() { return this._response; }
public get urlParams() { return this._urlParams; }
public verbose(v?: boolean) {
if (typeof v === "boolean") {
this._verbose = v;
return this;
} else {
return this._verbose;
}
}
public isSuccess(data: any, res: request.Response) {
return res.statusCode >= 200 && res.statusCode < 300;
}
public isBadRequest(data: any, res: request.Response) {
return res.statusCode === 400;
}
public isNotFound(data: any, res: request.Response) {
return res.statusCode === 404;
}
public isUnauthorized(data: any, res: request.Response) {
return res.statusCode === 401;
}
public isForbidden(data: any, res: request.Response) {
return res.statusCode === 403;
}
public isClientError(data: any, res: request.Response) {
return res.statusCode >= 400 && res.statusCode < 500;
}
public validateResponse(data: any, res: request.Response, reqData: any) {
if (res.statusCode === 204) {
return;
}
let resContentType = res.headers["content-type"];
if (resContentType) {
resContentType = resContentType.split(";")[0].trim().toLowerCase();
}
if (this.response.contentType !== resContentType) {
throw new Error("Content-Type is not match: " + resContentType);
}
if (resContentType === ContentType.JSON) {
this.response.data.validate(data, data, reqData);
}
}
public test() {
function testContentType(value: string) {
if (!Object.keys(ContentType).find((key: string) => (ContentType as any)[key] === value)) {
throw new Error("Invalid ContentType: " + value);
}
}
function testEndpoint(value: string) {
if (!value) {
throw new Error("endpoint is not defined");
}
if (value.charAt(0) !== "/") {
throw new Error("endpoint must starts with '/'");
}
}
function testMethod(value: string) {
if (!value) {
throw new Error("method is not defined");
}
if (!Object.keys(Method).find((key: string) => (Method as any)[key] === value)) {
throw new Error("Invalid method: " + value);
}
}
function testRulesReference(params: IParam, rules: any) {
const pool = new ParamPool(params);
const ret: Array<string> = [];
Object.keys(rules).forEach(key => {
if (!pool.getParams(key)) {
ret.push(key);
}
});
if (ret.length > 0) {
throw new Error("Undefined parameters : " + JSON.stringify(ret));
}
}
function testRequest(req: Request) {
testContentType(req.contentType);
if (req.params) {
req.params.test();
}
if (req.params && req.rules) {
testRulesReference(req.params, req.rules);
}
}
function testResponse(response: Response) {
if (!response) {
throw new Error("response is not defined.");
}
if (!response.data) {
throw new Error("data is not defined.");
}
response.data.test();
if (response.data && response.rules) {
testRulesReference(response.data, response.rules);
}
}
testEndpoint(this.endpoint);
testMethod(this.method);
testRequest(this.request);
testResponse(this.response);
}
public testCoverage() {
if (this.request && this.request.params && this.request.params.coverage() !== 1) {
throw new Error(`Request parameter [${this.request.params.uncoveredParamNames()}] are not covered.`);
}
}
public clearCoverage() {
if (this.request && this.request.params) {
this.request.params.clearCoverage();
}
}
private init(config: ApiConfig) {
this._name = config.name || config.endpoint;
this._endpoint = config.endpoint;
this._method = config.method;
this._request = new Request(config.request || {});
if (config.response) {
this._response = new Response(config.response);
}
if (config.endpoint) {
const urlParams = config.endpoint.match(/\[[^\]]*\]/g);
if (urlParams && urlParams.length > 0) {
this._urlParams = urlParams.map(v => v.substring(1, v.length - 1));
}
}
}
}