From 98274269847404630d89d68c018798eefeb32ed7 Mon Sep 17 00:00:00 2001 From: Bryan MacFarlane Date: Sat, 1 Feb 2020 18:39:08 -0500 Subject: [PATCH] json apis --- __tests__/basics.test.ts | 47 ++++++++++++++++ index.ts | 113 ++++++++++++++++++++++++++++++++++++++- interfaces.ts | 1 + 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/__tests__/basics.test.ts b/__tests__/basics.test.ts index 41105c6..f70d653 100644 --- a/__tests__/basics.test.ts +++ b/__tests__/basics.test.ts @@ -4,6 +4,13 @@ import * as fs from 'fs'; let sampleFilePath: string = path.join(__dirname, 'testoutput.txt'); +interface HttpBinData { + url: string; + data: any; + json: any; + args?: any +} + describe('basics', () => { let _http: httpm.HttpClient; @@ -191,5 +198,45 @@ describe('basics', () => { expect(res.message.statusCode).toBe(404); let body: string = await res.readBody(); done(); + }); + + it('gets a json object', async() => { + let jsonObj: httpm.ITypedResponse = await _http.getJson('https://httpbin.org/get'); + expect(jsonObj.statusCode).toBe(200); + expect(jsonObj.result).toBeDefined(); + expect(jsonObj.result.url).toBe('https://httpbin.org/get'); + }); + + it('getting a non existent json object returns null', async() => { + let jsonObj: httpm.ITypedResponse = await _http.getJson('https://httpbin.org/status/404'); + expect(jsonObj.statusCode).toBe(404); + expect(jsonObj.result).toBeNull(); + }); + + it('posts a json object', async() => { + let res: any = { name: 'foo' }; + let restRes: httpm.ITypedResponse = await _http.postJson('https://httpbin.org/post', res); + expect(restRes.statusCode).toBe(200); + expect(restRes.result).toBeDefined(); + expect(restRes.result.url).toBe('https://httpbin.org/post'); + expect(restRes.result.json.name).toBe('foo'); + }); + + it('puts a json object', async() => { + let res: any = { name: 'foo' }; + let restRes: httpm.ITypedResponse = await _http.putJson('https://httpbin.org/put', res); + expect(restRes.statusCode).toBe(200); + expect(restRes.result).toBeDefined(); + expect(restRes.result.url).toBe('https://httpbin.org/put'); + expect(restRes.result.json.name).toBe('foo'); + }); + + it('patch a json object', async() => { + let res: any = { name: 'foo' }; + let restRes: httpm.ITypedResponse = await _http.patchJson('https://httpbin.org/patch', res); + expect(restRes.statusCode).toBe(200); + expect(restRes.result).toBeDefined(); + expect(restRes.result.url).toBe('https://httpbin.org/patch'); + expect(restRes.result.json.name).toBe('foo'); }); }) diff --git a/index.ts b/index.ts index 2f7b475..fdf8408 100644 --- a/index.ts +++ b/index.ts @@ -71,6 +71,12 @@ export class HttpClientResponse implements ifm.IHttpClientResponse { } } +export interface ITypedResponse { + statusCode: number, + result: T | null, + headers: Object +} + export function isHttps(requestUrl: string) { let parsedUrl: url.Url = url.parse(requestUrl); return parsedUrl.protocol === 'https:'; @@ -162,6 +168,33 @@ export class HttpClient { return this.request(verb, requestUrl, stream, additionalHeaders); } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + public async getJson(requestUrl: string, additionalHeaders?: ifm.IHeaders): Promise> { + let res: ifm.IHttpClientResponse = await this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + + public async postJson(requestUrl: string, obj:T, additionalHeaders?: ifm.IHeaders): Promise> { + let data: string = JSON.stringify(obj, null, 2); + let res: ifm.IHttpClientResponse = await this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + + public async putJson(requestUrl: string, obj:T, additionalHeaders?: ifm.IHeaders): Promise> { + let data: string = JSON.stringify(obj, null, 2); + let res: ifm.IHttpClientResponse = await this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + + public async patchJson(requestUrl: string, obj:T, additionalHeaders?: ifm.IHeaders): Promise> { + let data: string = JSON.stringify(obj, null, 2); + let res: ifm.IHttpClientResponse = await this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + /** * Makes a raw http request. * All other methods such as get, post, patch, and request ultimately call this. @@ -358,7 +391,6 @@ export class HttpClient { info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort; info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); info.options.method = method; - info.options.headers = this._mergeHeaders(headers); if (this.userAgent != null) { info.options.headers["user-agent"] = this.userAgent; @@ -468,5 +500,82 @@ export class HttpClient { retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); const ms: number = ExponentialBackoffTimeSlice*Math.pow(2, retryNumber); return new Promise(resolve => setTimeout(()=>resolve(), ms)); - } + } + + private static dateTimeDeserializer(key: any, value: any): any { + if (typeof value === 'string'){ + let a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + + return value; + } + + private async _processResponse(res: ifm.IHttpClientResponse, options: ifm.IRequestOptions): Promise> { + return new Promise>(async (resolve, reject) => { + const statusCode: number = res.message.statusCode; + + const response: ITypedResponse = { + statusCode: statusCode, + result: null, + headers: {} + }; + + // not found leads to null obj returned + if (statusCode == HttpCodes.NotFound) { + resolve(response); + } + + let obj: any; + let contents: string; + + // get the result from the body + try { + contents = await res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, HttpClient.dateTimeDeserializer); + } else { + obj = JSON.parse(contents); + } + + response.result = obj; + } + + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg: string; + + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } else { + msg = "Failed request: (" + statusCode + ")"; + } + + let err: Error = new Error(msg); + + // attach statusCode and body obj (if available) to the error object + err['statusCode'] = statusCode; + if (response.result) { + err['result'] = response.result; + } + + reject(err); + } else { + resolve(response); + } + }); + } } diff --git a/interfaces.ts b/interfaces.ts index ae16eac..9369d11 100644 --- a/interfaces.ts +++ b/interfaces.ts @@ -42,6 +42,7 @@ export interface IRequestOptions { maxRedirects?: number; maxSockets?: number; keepAlive?: boolean; + deserializeDates?: boolean; // Allows retries only on Read operations (since writes may not be idempotent) allowRetries?: boolean; maxRetries?: number;