Challenge #11: Design Changes

Up until now, the Rot13Client has used transformAsync() for its interface. The original code did too, at first, but eventually it had to migrate to transform() so it could support cancelling requests. In this challenge, you’ll perform the same migration.

Instructions

  1. Remove the async keyword from rot13Client.transformAsync().
  2. Refactor it to use the following signature:
    const { transformPromise } = transform(port, text, correlationId) { ... }
  3. Make the tests pass again.

Remember to commit your changes when you’re done.

API Documentation

const { transformPromise } = rot13Client.transform(port, text, correlationId);

Call the ROT-13 service. This is just like transformAsync(), except that it has the ability to return multiple values. Use it like this:

const { transformPromise } = rot13Client.transform(port, text, correlationId);
const transformedText = await transformPromise;
  • port (number) - the port of the ROT-13 service
  • text (string) - the text to send to the ROT-13 service
  • correlationId (string) - a unique ID representing the user’s request
  • returns transformPromise (Promise<string>) - the encoded text returned by the ROT-13 service

JavaScript Primers

Hints

1
Start by modifying the test to not await the rot13Client.transformAsync() method. (See the Using Promises primer.)
It only needs a small change in the transformAsync() test helper.
async function transformAsync({
	rot13Client,
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	rot13Client = rot13Client ?? new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const responsePromise = rot13Client.transformAsync(port, text, correlationId);
	const response = await responsePromise;

	return { response, rot13Requests, httpRequests };
}
2
Before the async keyword can be removed from rot13Client.transformAsync(), its usage of await has to be removed.
Change rot13Client.transformAsync()’s call to #performRequestAsync() to store the result in a promise rather than awaiting it.
async transformAsync(port, text, correlationId) {
	const responsePromise = this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponse(responsePromise, port);
}
3
All the tests will fail. (TypeScript won’t even compile.)
This is because #parseResponse is no longer receiving the response; it’s receiving a promise. In JavaScript, none of the parsing logic is working, and in TypeScript, the compiler is seeing the type mismatch.
#parseResponse() needs to turn the promise into a response.
await the responsePromise.
You’ll also need to add the async keyword to the #parseResponse() signature.
Update the variable and method names accordingly.
async transformAsync(port, text, correlationId) {
	const responsePromise = this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponseAsync(responsePromise, port);
}

// ...

async #parseResponseAsync(responsePromise, port) {
	const response = await responsePromise;

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

In TypeScript, you’ll need to update the responsePromise type:

async #parseResponseAsync(responsePromise: Promise<HttpClientResponse>, port: number) {
	const response = await responsePromise;

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}
4
Now you can remove the async keyword from rot13Client.transformAsync()
Rename the method accordingly.

Test helper:

async function transformAsync({
	rot13Client,
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	rot13Client = rot13Client ?? new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const responsePromise = rot13Client.transform(port, text, correlationId);
	const response = await responsePromise;

	return { response, rot13Requests, httpRequests };
}

Production code:

transform(port, text, correlationId) {
	const responsePromise = this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponseAsync(responsePromise, port);
}

In TypeScript, you’ll need to remove the placeholder transform() method (at the bottom of the class). The code won’t compile until you change the return type in the next step.

5
You’re ready to change the return type to match the desired signature.
You need to return { transformPromise }.
Object shorthand and destructuring are all you need.

Test helper:

async function transformAsync({
	rot13Client,
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	rot13Client = rot13Client ?? new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const { transformPromise } = rot13Client.transform(port, text, correlationId);
	const response = await transformPromise;

	return { response, rot13Requests, httpRequests };
}

Production code:

transform(port, text, correlationId) {
	const responsePromise = this.#performRequestAsync(port, text, correlationId);
	const transformPromise = this.#parseResponseAsync(responsePromise, port);

	return { transformPromise };
}

TypeScript will need a return type declaration and a placeholder implementation of cancelFn(). cancelFn() isn’t part of this challenge, but the rest of the codebase depends upon it.

transform(
	port: number,
	text: string,
	correlationId: string,
): {
	transformPromise: Promise<string>,
	cancelFn: () => void
} {
	const responsePromise = this.#performRequestAsync(port, text, correlationId);
	const transformPromise = this.#parseResponseAsync(responsePromise, port);

	return {
		transformPromise,
		cancelFn() { throw new Error("not implemented"); },
	};
}

Complete Solution

Test code (JavaScript):
describe.only("ROT-13 Service client", () => {

	describe("happy path", () => {

		// Challenge #1
		it("makes request", async () => {
			const { httpRequests } = await transformAsync({
				port: 9999,
				text: "text_to_transform",
				correlationId: "my-correlation-id"
			});

			assert.deepEqual(httpRequests.data, [{
				host: HOST,
				port: 9999,
				path: "/rot13/transform",
				method: "post",
				headers: {
					"content-type": "application/json",
					"x-correlation-id": "my-correlation-id",
				},
				body: JSON.stringify({ text: "text_to_transform" }),
			}]);
		});

		// Challenge #3
		it("parses response", async () => {
			const { response } = await transformAsync({
				rot13ServiceStatus: VALID_ROT13_STATUS,
				rot13ServiceHeaders: VALID_ROT13_HEADERS,
				rot13ServiceBody: VALID_ROT13_BODY,
			});

			assert.equal(response, VALID_RESPONSE);
		});

		// Challenge #4
		it("tracks requests", async () => {
			const { rot13Requests } = await transformAsync({
				port: 9999,
				text: "my text",
				correlationId: "my-correlation-id",
			});

			assert.deepEqual(rot13Requests.data, [{
				port: 9999,
				text: "my text",
				correlationId: "my-correlation-id",
			}]);
		});

	});


	describe("failure paths", () => {

		// Challenge #7
		it("fails gracefully when status code has unexpected value", async () => {
			await assertFailureAsync({
				rot13ServiceStatus: 400,
				message: "Unexpected status from ROT-13 service",
			});
		});

		// Challenge #10 (1 of 4)
		it("fails gracefully if body doesn't exist", async () => {
			await assertFailureAsync({
				rot13ServiceBody: "",
				message: "Body missing from ROT-13 service",
			});
		});

		// Challenge #10 (2 of 4)
		it("fails gracefully if body is unparseable", async () => {
			await assertFailureAsync({
				rot13ServiceBody: "xxx",
				message: "Unparseable body from ROT-13 service: Unexpected token x in JSON at position 0",
			});
		});

		// Challenge #10 (3 of 4)
		it("fails gracefully if body has unexpected value", async () => {
			await assertFailureAsync({
				rot13ServiceBody: JSON.stringify({ foo: "bar" }),
				message: "Unexpected body from ROT-13 service: body.transformed must be a string, but it was undefined",
			});
		});

		// Challenge #10 (4 of 4)
		it("doesn't fail when body has more fields than we expect", async () => {
			const rot13ServiceBody = JSON.stringify({ transformed: "response", foo: "bar" });
			await assert.doesNotThrowAsync(
				() => transformAsync({ rot13ServiceBody }),
			);
		});

	});


	describe("cancellation", () => {

		// Bonus Challenge #2
		it("can cancel requests", async () => {
			// to do
		});

		// Bonus Challenge #3 (1 of 2)
		it("tracks requests that are cancelled", async () => {
			// to do
		});

		// Bonus Challenge #3 (2 of 2)
		it("doesn't track attempted cancellations that don't actually cancel the request", async () => {
			// to do
		});

	});


	describe("nulled instance", () => {

		// Challenge #5
		it("provides default response", async () => {
			const rot13Client = Rot13Client.createNull();
			const { response } = await transformAsync({ rot13Client });
			assert.equal(response, "Nulled Rot13Client response");
		});

		// Challenge #6
		it("can configure multiple responses", async () => {
			const rot13Client = Rot13Client.createNull([
				{ response: "response 1" },
				{ response: "response 2" },
			]);

			const { response: response1 } = await transformAsync({ rot13Client });
			const { response: response2 } = await transformAsync({ rot13Client });

			assert.equal(response1, "response 1");
			assert.equal(response2, "response 2");
		});

		// Challenge #8
		it("simulates errors", async () => {
			const rot13Client = Rot13Client.createNull([{ error: "my error" }]);
			await assertFailureAsync({
				rot13Client,
				rot13ServiceStatus: 500,
				rot13ServiceHeaders: {},
				rot13ServiceBody: "my error",
				message: "Unexpected status from ROT-13 service",
			});
		});

		// Bonus Challenge #1
		it("simulates hangs", async () => {
			// to do
		});

	});

});

async function transformAsync({
	rot13Client,
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	rot13Client = rot13Client ?? new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const { transformPromise } = rot13Client.transform(port, text, correlationId);
	const response = await transformPromise;

	return { response, rot13Requests, httpRequests };
}

async function assertFailureAsync({
	rot13Client,
	port = 42,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
	message,
}) {
	const expectedError =
		`${message}\n` +
		`Host: ${HOST}:${port}\n` +
		"Endpoint: /rot13/transform\n" +
		`Status: ${rot13ServiceStatus}\n` +
		`Headers: ${JSON.stringify(rot13ServiceHeaders)}\n` +
		`Body: ${rot13ServiceBody}`;

	await assert.throwsAsync(
		() => transformAsync({
			rot13Client,
			port,
			rot13ServiceStatus,
			rot13ServiceHeaders,
			rot13ServiceBody,
		}),
		expectedError,
	);
}
Test code (TypeScript):
describe.only("ROT-13 Service client", () => {

	describe("happy path", () => {

		// Challenge #1
		it("makes request", async () => {
			const { httpRequests } = await transformAsync({
				port: 9999,
				text: "text_to_transform",
				correlationId: "my-correlation-id"
			});

			assert.deepEqual(httpRequests.data, [{
				host: HOST,
				port: 9999,
				path: "/rot13/transform",
				method: "post",
				headers: {
					"content-type": "application/json",
					"x-correlation-id": "my-correlation-id",
				},
				body: JSON.stringify({ text: "text_to_transform" }),
			}]);
		});

		// Challenge #3
		it("parses response", async () => {
			const { response } = await transformAsync({
				rot13ServiceStatus: VALID_ROT13_STATUS,
				rot13ServiceHeaders: VALID_ROT13_HEADERS,
				rot13ServiceBody: VALID_ROT13_BODY,
			});

			assert.equal(response, VALID_RESPONSE);
		});

		// Challenge #4
		it("tracks requests", async () => {
			const { rot13Requests } = await transformAsync({
				port: 9999,
				text: "my text",
				correlationId: "my-correlation-id",
			});

			assert.deepEqual(rot13Requests.data, [{
				port: 9999,
				text: "my text",
				correlationId: "my-correlation-id",
			}]);
		});

	});


	describe("failure paths", () => {

		// Challenge #7
		it("fails gracefully when status code has unexpected value", async () => {
			await assertFailureAsync({
				rot13ServiceStatus: 400,
				message: "Unexpected status from ROT-13 service",
			});
		});

		// Challenge #10 (1 of 4)
		it("fails gracefully if body doesn't exist", async () => {
			await assertFailureAsync({
				rot13ServiceBody: "",
				message: "Body missing from ROT-13 service",
			});
		});

		// Challenge #10 (2 of 4)
		it("fails gracefully if body is unparseable", async () => {
			await assertFailureAsync({
				rot13ServiceBody: "xxx",
				message: "Unparseable body from ROT-13 service: Unexpected token x in JSON at position 0",
			});
		});

		// Challenge #10 (3 of 4)
		it("fails gracefully if body has unexpected value", async () => {
			await assertFailureAsync({
				rot13ServiceBody: JSON.stringify({ foo: "bar" }),
				message: "Unexpected body from ROT-13 service: body.transformed must be a string, but it was undefined",
			});
		});

		// Challenge #10 (4 of 4)
		it("doesn't fail when body has more fields than we expect", async () => {
			const rot13ServiceBody = JSON.stringify({ transformed: "response", foo: "bar" });
			await assert.doesNotThrowAsync(
				() => transformAsync({ rot13ServiceBody }),
			);
		});

	});


	describe("cancellation", () => {

		// Bonus Challenge #2
		it("can cancel requests", async () => {
			// to do
		});

		// Bonus Challenge #3 (1 of 2)
		it("tracks requests that are cancelled", async () => {
			// to do
		});

		// Bonus Challenge #3 (2 of 2)
		it("doesn't track attempted cancellations that don't actually cancel the request", async () => {
			// to do
		});

	});


	describe("nulled instance", () => {

		// Challenge #5
		it("provides default response", async () => {
			const rot13Client = Rot13Client.createNull();
			const { response } = await transformAsync({ rot13Client });
			assert.equal(response, "Nulled Rot13Client response");
		});

		// Challenge #6
		it("can configure multiple responses", async () => {
			const rot13Client = Rot13Client.createNull([
				{ response: "response 1" },
				{ response: "response 2" },
			]);

			const { response: response1 } = await transformAsync({ rot13Client });
			const { response: response2 } = await transformAsync({ rot13Client });

			assert.equal(response1, "response 1");
			assert.equal(response2, "response 2");
		});

		// Challenge #8
		it("simulates errors", async () => {
			const rot13Client = Rot13Client.createNull([{ error: "my error" }]);
			await assertFailureAsync({
				rot13Client,
				rot13ServiceStatus: 500,
				rot13ServiceHeaders: {},
				rot13ServiceBody: "my error",
				message: "Unexpected status from ROT-13 service",
			});
		});

		// Bonus Challenge #1
		it("simulates hangs", async () => {
			// to do
		});

	});

});

interface TransformOptions {
	rot13Client?: Rot13Client,
	port?: number,
	text?: string,
	correlationId?: string,
	rot13ServiceStatus?: number,
	rot13ServiceHeaders?: HttpHeaders,
	rot13ServiceBody?: string,
}

async function transformAsync({
	rot13Client,
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
}: TransformOptions) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	rot13Client = rot13Client ?? new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const { transformPromise } = rot13Client.transform(port, text, correlationId);
	const response = await transformPromise;

	return { response, rot13Requests, httpRequests };
}

async function assertFailureAsync({
	rot13Client,
	port = 42,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
	message,
}: {
	rot13Client?: Rot13Client,
	port?: number,
	rot13ServiceStatus?: number,
	rot13ServiceHeaders?: HttpHeaders,
	rot13ServiceBody?: string,
	message?: string,
}) {
	const expectedError =
		`${message}\n` +
		`Host: ${HOST}:${port}\n` +
		"Endpoint: /rot13/transform\n" +
		`Status: ${rot13ServiceStatus}\n` +
		`Headers: ${JSON.stringify(rot13ServiceHeaders)}\n` +
		`Body: ${rot13ServiceBody}`;

	await assert.throwsAsync(
		() => transformAsync({
			rot13Client,
			port,
			rot13ServiceStatus,
			rot13ServiceHeaders,
			rot13ServiceBody,
		}),
		expectedError,
	);
}
Production code (JavaScript):
export class Rot13Client {

	static create() {
		ensure.signature(arguments, []);

		return new Rot13Client(HttpClient.create());
	}

	static createNull(options = [ {} ]) {
		const httpResponses = options.map((response) => nulledHttpResponse(response));

		const httpClient = HttpClient.createNull({
			[TRANSFORM_ENDPOINT]: httpResponses,
		});
		return new Rot13Client(httpClient);
	}

	constructor(httpClient) {
		ensure.signature(arguments, [ HttpClient ]);

		this._httpClient = httpClient;
		this._listener = OutputListener.create();
	}

	transform(port, text, correlationId) {
		const responsePromise = this.#performRequestAsync(port, text, correlationId);
		const transformPromise = this.#parseResponseAsync(responsePromise, port);

		return { transformPromise };
	}

	trackRequests() {
		return this._listener.trackOutput();
	}

	async #performRequestAsync(port, text, correlationId) {
		this._listener.emit({ port, text, correlationId });

		return await this._httpClient.requestAsync({
			host: HOST,
			port,
			method: "POST",
			path: "/rot13/transform",
			headers: {
				"content-type": "application/json",
				"x-correlation-id": correlationId,
			},
			body: JSON.stringify({ text }),
		});
	}

	async #parseResponseAsync(responsePromise, port) {
		const response = await responsePromise;

		if (response.status !== 200) {
			throwError("Unexpected status from ROT-13 service", port, response);
		}
		if (response.body === "") {
			throwError("Body missing from ROT-13 service", port, response);
		}

		let parsedBody;
		try {
			parsedBody = JSON.parse(response.body);
		}
		catch(err) {
			throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
		}

		const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
		if (typeError !== null) {
			throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
		}

		return parsedBody.transformed;
	}

}

function throwError(message, port, response) {
	throw new Error(
		`${message}\n` +
		`Host: ${HOST}:${port}\n` +
		`Endpoint: ${TRANSFORM_ENDPOINT}\n` +
		`Status: ${response.status}\n` +
		`Headers: ${JSON.stringify(response.headers)}\n` +
		`Body: ${response.body}`,
	);
}

function nulledHttpResponse({
	response = "Nulled Rot13Client response",
	error = undefined,
}) {
	if (error !== undefined) {
		return {
			status: 500,
			headers: {},
			body: error,
		};
	}
	else {
		return {
			status: 200,
			headers: {
				"content-type": "application/json",
			},
			body: JSON.stringify({
				transformed: response,
			}),
		};
	}
}
Production code (TypeScript):
export class Rot13Client {

	readonly _listener: OutputListener<Rot13ClientOutput>;

	static create(): Rot13Client {
		return new Rot13Client(HttpClient.create());
	}

	static createNull(options: NulledRot13ClientResponses = [ {} ]): Rot13Client {
		const httpResponses = options.map((response) => nulledHttpResponse(response));

		const httpClient = HttpClient.createNull({
			[TRANSFORM_ENDPOINT]: httpResponses,
		});
		return new Rot13Client(httpClient);
	}

	constructor(private readonly _httpClient: HttpClient) {
		this._listener = OutputListener.create();
	}

	transform(
		port: number,
		text: string,
		correlationId: string,
	): {
		transformPromise: Promise<string>,
		cancelFn: () => void
	} {
		const responsePromise = this.#performRequestAsync(port, text, correlationId);
		const transformPromise = this.#parseResponseAsync(responsePromise, port);

		return {
			transformPromise,
			cancelFn() { throw new Error("not implemented"); },
		};
	}

	trackRequests(): OutputTracker<Rot13ClientOutput> {
		return this._listener.trackOutput();
	}

	async #performRequestAsync(port: number, text: string, correlationId: string) {
		this._listener.emit({ port, text, correlationId });

		return await this._httpClient.requestAsync({
			host: HOST,
			port,
			method: "POST",
			path: "/rot13/transform",
			headers: {
				"content-type": "application/json",
				"x-correlation-id": correlationId,
			},
			body: JSON.stringify({ text }),
		});
	}

	async #parseResponseAsync(responsePromise: Promise<HttpClientResponse>, port: number) {
		const response = await responsePromise;

		if (response.status !== 200) {
			throwError("Unexpected status from ROT-13 service", port, response);
		}
		if (response.body === "") {
			throwError("Body missing from ROT-13 service", port, response);
		}

		let parsedBody;
		try {
			parsedBody = JSON.parse(response.body);
		}
		catch(err) {
			throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
		}

		const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
		if (typeError !== null) {
			throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
		}

		return parsedBody.transformed;
	}

}

function nulledHttpResponse({
	response = "Nulled Rot13Client response",
	error = undefined,
}: NulledRot13ClientResponse): NulledHttpClientResponse {
	if (error !== undefined) {
		return {
			status: 500,
			headers: {},
			body: error,
		};
	}
	else {
		return {
			status: 200,
			headers: {
				"content-type": "application/json",
			},
			body: JSON.stringify({
				transformed: response,
			}),
		};
	}
}

function throwError(message: string, port: number, response: HttpClientResponse) {
	throw new Error(
		`${message}\n` +
		`Host: ${HOST}:${port}\n` +
		`Endpoint: ${TRANSFORM_ENDPOINT}\n` +
		`Status: ${response.status}\n` +
		`Headers: ${JSON.stringify(response.headers)}\n` +
		`Body: ${response.body}`,
	);
}

Bonus challenges

Return to module overview