From a080016daf5f17d46e01b8876edefd893b50fc1c Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 13 Sep 2023 21:33:47 -0700 Subject: [PATCH] refactor: making the library async --- .vscode/settings.json | 10 ++++- README.md | 4 ++ src/fixtures/runCustomFixtures.ts | 7 ++- src/index.test.ts | 72 +++++++++++++++++++++---------- src/index.ts | 35 ++++++++++----- src/targets/index.test.ts | 16 ++++--- 6 files changed, 101 insertions(+), 43 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f3efd0c24..2472a73d7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,10 @@ { - "files.insertFinalNewline": false, // controlled by the .editorconfig at root since we can't map vscode settings directly to files (see: https://github.com/microsoft/vscode/issues/35350) - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.codeActionsOnSave": { + "source.fixAll": true + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + + // controlled by the .editorconfig at root since we can't map vscode settings directly to files + // https://github.com/microsoft/vscode/issues/35350 + "files.insertFinalNewline": false } diff --git a/README.md b/README.md index bafc6553b..a047ce968 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ const snippet = new HTTPSnippet({ url: 'httsp://httpbin.org/anything', }); +await snippet.init(); + // generate Node.js: Native output console.log(snippet.convert('node')); @@ -102,6 +104,8 @@ const snippet = new HTTPSnippet({ url: 'https://httpbin.org/anything', }); +await snippet.init(); + // generate Shell: cURL output console.log( snippet.convert('shell', 'curl', { diff --git a/src/fixtures/runCustomFixtures.ts b/src/fixtures/runCustomFixtures.ts index dab522f8a..d5cd8f4b4 100644 --- a/src/fixtures/runCustomFixtures.ts +++ b/src/fixtures/runCustomFixtures.ts @@ -24,13 +24,16 @@ export interface CustomFixture { export const runCustomFixtures = ({ targetId, clientId, tests }: CustomFixture) => { describe(`custom fixtures for ${targetId}:${clientId}`, () => { - tests.forEach(({ it: title, expected: fixtureFile, options, input: request }) => { + tests.forEach(async ({ it: title, expected: fixtureFile, options, input: request }) => { const opts: HTTPSnippetOptions = {}; if (options.harIsAlreadyEncoded) { opts.harIsAlreadyEncoded = options.harIsAlreadyEncoded; } - const result = new HTTPSnippet(request, opts).convert(targetId, clientId, options); + const snippet = new HTTPSnippet(request, opts); + await snippet.init(); + + const result = snippet.convert(targetId, clientId, options); const filePath = path.join(__dirname, '..', 'targets', targetId, clientId, 'fixtures', fixtureFile); if (process.env.OVERWRITE_EVERYTHING) { writeFileSync(filePath, String(result)); diff --git a/src/index.test.ts b/src/index.test.ts index 3d9fba26d..972ddbe66 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -10,21 +10,32 @@ import short from './fixtures/requests/short.cjs'; import { HTTPSnippet } from './index.js'; describe('HTTPSnippet', () => { - it('should return false if no matching target', () => { + it('should return false if no matching target', async () => { const snippet = new HTTPSnippet(short.log.entries[0].request as Request); + await snippet.init(); + // @ts-expect-error intentionally incorrect const result = snippet.convert(null); expect(result).toBe(false); }); + it('should throw an error if you try to convert before initializing', () => { + const snippet = new HTTPSnippet(short.log.entries[0].request as Request); + + expect(() => { + snippet.convert('node'); + }).toThrow(new Error('The `.init()` method must be invoked before `.convert()`.')); + }); + describe('repair malformed `postData`', () => { - it('should repair a HAR with an empty `postData` object', () => { + it('should repair a HAR with an empty `postData` object', async () => { const snippet = new HTTPSnippet({ method: 'POST', url: 'https://httpbin.org/anything', postData: {}, } as Request); + await snippet.init(); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -32,7 +43,7 @@ describe('HTTPSnippet', () => { }); }); - it('should repair a HAR with a `postData` params object missing `mimeType`', () => { + it('should repair a HAR with a `postData` params object missing `mimeType`', async () => { // @ts-expect-error Testing a malformed HAR case. const snippet = new HTTPSnippet({ method: 'POST', @@ -41,6 +52,7 @@ describe('HTTPSnippet', () => { params: [], }, } as Request); + await snippet.init(); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -49,7 +61,7 @@ describe('HTTPSnippet', () => { }); }); - it('should repair a HAR with a `postData` text object missing `mimeType`', () => { + it('should repair a HAR with a `postData` text object missing `mimeType`', async () => { const snippet = new HTTPSnippet({ method: 'POST', url: 'https://httpbin.org/anything', @@ -57,6 +69,7 @@ describe('HTTPSnippet', () => { text: '', }, } as Request); + await snippet.init(); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -66,7 +79,7 @@ describe('HTTPSnippet', () => { }); }); - it('should parse HAR file with multiple entries', () => { + it('should parse HAR file with multiple entries', async () => { const snippet = new HTTPSnippet({ log: { version: '1.2', @@ -91,6 +104,8 @@ describe('HTTPSnippet', () => { }, }); + await snippet.init(); + expect(snippet).toHaveProperty('requests'); expect(Array.isArray(snippet.requests)).toBeTruthy(); expect(snippet.requests).toHaveLength(2); @@ -129,25 +144,29 @@ describe('HTTPSnippet', () => { ] as { expected: string; input: keyof typeof mimetypes; - }[])('mimetype conversion of $input to $output', ({ input, expected }) => { + }[])('mimetype conversion of $input to $output', async ({ input, expected }) => { const snippet = new HTTPSnippet(mimetypes[input]); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.postData.mimeType).toStrictEqual(expected); }); }); - it('should set postData.text to empty string when postData.params is undefined in application/x-www-form-urlencoded', () => { + it('should set postData.text to empty string when postData.params is undefined in application/x-www-form-urlencoded', async () => { const snippet = new HTTPSnippet(mimetypes['application/x-www-form-urlencoded']); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.postData.text).toBe(''); }); describe('requestExtras', () => { describe('uriObj', () => { - it('should add uriObj', () => { + it('should add uriObj', async () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); + await snippet.init(); + const request = snippet.requests[0]; expect(request.uriObj).toMatchObject({ @@ -170,35 +189,38 @@ describe('HTTPSnippet', () => { }); }); - it('should fix the `path` property of uriObj to match queryString', () => { + it('should fix the `path` property of uriObj to match queryString', async () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.uriObj.path).toBe('/anything?foo=bar&foo=baz&baz=abc&key=value'); }); }); describe('queryObj', () => { - it('should add queryObj', () => { + it('should add queryObj', async () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.queryObj).toMatchObject({ baz: 'abc', key: 'value', foo: ['bar', 'baz'] }); }); }); describe('headersObj', () => { - it('should add headersObj', () => { + it('should add headersObj', async () => { const snippet = new HTTPSnippet(headers.log.entries[0].request as Request); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.headersObj).toMatchObject({ accept: 'application/json', 'x-foo': 'Bar', }); }); - it('should add headersObj to source object case insensitive when HTTP/1.0', () => { + it('should add headersObj to source object case insensitive when HTTP/1.0', async () => { const snippet = new HTTPSnippet({ ...headers.log.entries[0].request, httpVersion: 'HTTP/1.1', @@ -211,6 +233,8 @@ describe('HTTPSnippet', () => { ], } as Request); + await snippet.init(); + const request = snippet.requests[0]; expect(request.headersObj).toMatchObject({ @@ -220,7 +244,7 @@ describe('HTTPSnippet', () => { }); }); - it('should add headersObj to source object lowercased when HTTP/2.x', () => { + it('should add headersObj to source object lowercased when HTTP/2.x', async () => { const snippet = new HTTPSnippet({ ...headers.log.entries[0].request, httpVersion: 'HTTP/2', @@ -233,6 +257,8 @@ describe('HTTPSnippet', () => { ], } as Request); + await snippet.init(); + const request = snippet.requests[0]; expect(request.headersObj).toMatchObject({ @@ -244,19 +270,21 @@ describe('HTTPSnippet', () => { }); describe('url', () => { - it('should modify the original url to strip query string', () => { + it('should modify the original url to strip query string', async () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.url).toBe('https://httpbin.org/anything'); }); }); describe('fullUrl', () => { - it('adds fullURL', () => { + it('adds fullURL', async () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - const request = snippet.requests[0]; + await snippet.init(); + const request = snippet.requests[0]; expect(request.fullUrl).toBe('https://httpbin.org/anything?foo=bar&foo=baz&baz=abc&key=value'); }); }); diff --git a/src/index.ts b/src/index.ts index 07283d5f4..5300cd5de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,12 +74,16 @@ const isHarEntry = (value: any): value is HarEntry => Array.isArray(value.log.entries); export class HTTPSnippet { + initCalled = false; + + entries: Entry[] = []; + requests: Request[] = []; - constructor(input: HarEntry | HarRequest, opts: HTTPSnippetOptions = {}) { - let entries: Entry[] = []; + options: HTTPSnippetOptions = {}; - const options = { + constructor(input: HarEntry | HarRequest, opts: HTTPSnippetOptions = {}) { + this.options = { harIsAlreadyEncoded: false, ...opts, }; @@ -89,16 +93,19 @@ export class HTTPSnippet { // is it har? if (isHarEntry(input)) { - entries = input.log.entries; + this.entries = input.log.entries; } else { - entries = [ + this.entries = [ { request: input, }, ]; } + } - entries.forEach(({ request }) => { + init() { + this.initCalled = true; + this.entries.forEach(({ request }) => { // add optional properties to make validation successful const req = { bodySize: 0, @@ -118,11 +125,13 @@ export class HTTPSnippet { req.postData.mimeType = 'application/octet-stream'; } - this.requests.push(this.prepare(req as HarRequest, options)); + this.requests.push(this.prepare(req as HarRequest, this.options)); }); + + return this; } - prepare = (harRequest: HarRequest, options: HTTPSnippetOptions) => { + prepare(harRequest: HarRequest, options: HTTPSnippetOptions) { const request: Request = { ...harRequest, fullUrl: '', @@ -334,9 +343,13 @@ export class HTTPSnippet { url, uriObj, }; - }; + } + + convert(targetId: TargetId, clientId?: ClientId, options?: any) { + if (!this.initCalled) { + throw new Error('The `.init()` method must be invoked before `.convert()`.'); + } - convert = (targetId: TargetId, clientId?: ClientId, options?: any) => { if (!options && clientId) { options = clientId; } @@ -349,5 +362,5 @@ export class HTTPSnippet { const { convert } = target.clientsById[clientId || target.info.default]; const results = this.requests.map(request => convert(request, options)); return results.length === 1 ? results[0] : results; - }; + } } diff --git a/src/targets/index.test.ts b/src/targets/index.test.ts index fe40b68bb..6d7e057a5 100644 --- a/src/targets/index.test.ts +++ b/src/targets/index.test.ts @@ -59,7 +59,7 @@ availableTargets() describe(`${title} Request Validation`, () => { clients.filter(testFilter('key', clientFilter)).forEach(({ key: clientId, extname: fixtureExtension }) => { describe(`${clientId}`, () => { - fixtures.filter(testFilter(0, fixtureFilter)).forEach(([fixture, request]) => { + fixtures.filter(testFilter(0, fixtureFilter)).forEach(async ([fixture, request]) => { const expectedPath = path.join( 'src', 'targets', @@ -77,8 +77,10 @@ availableTargets() } const expected = readFileSync(expectedPath).toString(); - const { convert } = new HTTPSnippet(request, options); - const result = convert(targetId, clientId); //? + const snippet = new HTTPSnippet(request, options); + await snippet.init(); + + const result = snippet.convert(targetId, clientId); if (OVERWRITE_EVERYTHING && result) { writeFileSync(expectedPath, String(result)); @@ -286,7 +288,7 @@ describe('addTargetClient', () => { delete targets.node.clientsById.custom; }); - it('should add a new custom target', () => { + it('should add a new custom target', async () => { const customClient: Client = { info: { key: 'custom', @@ -302,8 +304,10 @@ describe('addTargetClient', () => { addTargetClient('node', customClient); - const { convert } = new HTTPSnippet(short.log.entries[0].request as Request, {}); - const result = convert('node', 'custom'); + const snippet = new HTTPSnippet(short.log.entries[0].request as Request, {}); + await snippet.init(); + + const result = snippet.convert('node', 'custom'); expect(result).toBe('This was generated from a custom client.'); });