From f0925a4e757456215595a73fc97cda5450799667 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Mon, 8 May 2023 17:56:21 +0900 Subject: [PATCH] feat: add utility functions --- _dev_deps.ts | 10 ++++ create.ts | 37 ++++++++++++++ create_test.ts | 49 ++++++++++++++++++ deno.lock | 13 +++++ deps.ts | 5 ++ equal.ts | 88 ++++++++++++++++++++++++++++++++ equal_test.ts | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ is.ts | 18 +++++++ is_test.ts | 36 +++++++++++++ 9 files changed, 392 insertions(+) create mode 100644 _dev_deps.ts create mode 100644 create.ts create mode 100644 create_test.ts create mode 100644 deno.lock create mode 100644 deps.ts create mode 100644 equal.ts create mode 100644 equal_test.ts create mode 100644 is.ts create mode 100644 is_test.ts diff --git a/_dev_deps.ts b/_dev_deps.ts new file mode 100644 index 0000000..8d5c6e8 --- /dev/null +++ b/_dev_deps.ts @@ -0,0 +1,10 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. + +export { describe, it } from "https://deno.land/std@0.185.0/testing/bdd.ts"; +export { + assert, + assertEquals, + assertFalse, + assertRejects, + assertThrows, +} from "https://deno.land/std@0.185.0/testing/asserts.ts"; diff --git a/create.ts b/create.ts new file mode 100644 index 0000000..742f63a --- /dev/null +++ b/create.ts @@ -0,0 +1,37 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. +// This module is browser compatible. + +import { isResponse } from "./is.ts"; + +/** Create a new `Response`. + * + * If you create a new `Response` from an existing `Response`, any options you set + * in an options argument for the new response replace any corresponding options + * set in the original `Response`. + * + * @example + * ```ts + * import { createResponse } from "https://deno.land/x/response_utils@$VERSION/create.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const init: Response; + * const response = createResponse(init, { status: 201 }); + * + * assertEquals(response.status, 201); + * ``` + */ +export function createResponse( + input: Response | Body, + init?: ResponseInit, +): Response { + init = isResponse(input) + ? { + headers: input.headers, + status: input.status, + statusText: input.statusText, + ...init, + } + : init; + + return new Response(input.body, init); +} diff --git a/create_test.ts b/create_test.ts new file mode 100644 index 0000000..a733dd2 --- /dev/null +++ b/create_test.ts @@ -0,0 +1,49 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. + +import { createResponse } from "./create.ts"; +import { equalsResponse } from "./equal.ts"; +import { assert, assertEquals, describe, it } from "./_dev_deps.ts"; + +describe("createResponse", () => { + it("should return new response", async () => { + const init = new Response(); + const response = createResponse(init); + + assert(init !== response); + assert(await equalsResponse(init, response, true)); + }); + + it("should return partial updated response", () => { + const init = new Response(null, { status: 201 }); + const response = createResponse(init, { status: 202 }); + + assertEquals(response.status, 202); + }); + + it("should extern body", async () => { + const response = createResponse(new Response("test"), { status: 202 }); + + assert( + await equalsResponse( + response, + new Response("test", { status: 202 }), + true, + ), + ); + }); + + it("should not merge headers", async () => { + const response = createResponse( + new Response(null, { headers: { "x-test": "test" } }), + { headers: { "x-test2": "test2" } }, + ); + + assert( + await equalsResponse( + response, + new Response(null, { headers: { "x-test2": "test2" } }), + true, + ), + ); + }); +}); diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..1f99a62 --- /dev/null +++ b/deno.lock @@ -0,0 +1,13 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.185.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.185.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.185.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.185.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.185.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/std@0.185.0/testing/bdd.ts": "59f7f7503066d66a12e50ace81bfffae5b735b6be1208f5684b630ae6b4de1d0", + "https://deno.land/x/headers_utils@1.0.0/equal.ts": "ef72aa7e96fe317df43f8e5948b4bb4e3c0d5935cfa4984a335aeb7ecf51f11c", + "https://deno.land/x/isx@1.3.1/is_null.ts": "02b30255073843d001e715a04382f1d6aebd77ed5506ffbb44bf77b9e20ebf7d" + } +} diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..c82716e --- /dev/null +++ b/deps.ts @@ -0,0 +1,5 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. +// This module is browser compatible. + +export { equalsHeaders } from "https://deno.land/x/headers_utils@1.0.0/equal.ts"; +export { isNull } from "https://deno.land/x/isx@1.3.1/is_null.ts"; diff --git a/equal.ts b/equal.ts new file mode 100644 index 0000000..11c0023 --- /dev/null +++ b/equal.ts @@ -0,0 +1,88 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. +// This module is browser compatible. + +import { equalsHeaders, isNull } from "./deps.ts"; + +/** Check two `Response` fields equality. + * + * @example + * ```ts + * import { equalsResponse } from "https://deno.land/x/response_utils@$VERSION/equal.ts"; + * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * + * assert( + * equalsResponse( + * new Response(null, { status: 204, headers: { "content-length": "0" } }), + * new Response(null, { status: 204, headers: { "content-length": "0" } }), + * ), + * ); + * ``` + */ +export function equalsResponse(left: Response, right: Response): boolean; +/** Strict check two `Response` fields equality. + * + * @example + * ```ts + * import { equalsResponse } from "https://deno.land/x/response_utils@$VERSION/equal.ts"; + * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * + * assert( + * await equalsResponse( + * new Response("test1", { status: 200, headers: { "content-length": "5" } }), + * new Response("test2", { status: 200, headers: { "content-length": "5" } }), + * false, + * ), + * ); + * ``` + * + * @throws {Error} In strict mode, if response body has already been read. + */ +export function equalsResponse( + left: Response, + right: Response, + strict: boolean, +): boolean | Promise; +export function equalsResponse( + left: Response, + right: Response, + strict?: boolean, +): boolean | Promise { + strict ??= false; + + const staticResult = left.ok === right.ok && + left.bodyUsed === right.bodyUsed && + left.redirected === right.redirected && + left.status === right.status && + left.statusText === right.statusText && + left.type === right.type && + left.url === right.url && + equalsBodyType(left.body, right.body) && + equalsHeaders(left.headers, right.headers); + + if (!staticResult || !strict) return staticResult; + + if (left.bodyUsed || right.bodyUsed) { + throw Error( + "response body has already been read and the body cannot be strictly compared", + ); + } + + return Promise.all([left.clone().text(), right.clone().text()]).then(( + [left, right], + ) => Object.is(left, right)); +} + +function equalsBodyType( + left: Response["body"], + right: Response["body"], +): boolean { + if (isNull(left)) { + return isNull(right); + } + + if (isNull(right)) { + return isNull(left); + } + + return true; +} diff --git a/equal_test.ts b/equal_test.ts new file mode 100644 index 0000000..736de5e --- /dev/null +++ b/equal_test.ts @@ -0,0 +1,136 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. + +import { equalsResponse } from "./equal.ts"; +import { + assert, + assertEquals, + assertFalse, + assertThrows, + describe, + it, +} from "./_dev_deps.ts"; + +describe("equalsResponse", () => { + it("should pass cases", () => { + const table: [Response, Response, boolean][] = [ + [new Response(), new Response(), true], + [new Response(null), new Response(), true], + [new Response(undefined), new Response(), true], + [ + new Response(null, { + status: 500, + }), + new Response(null, { + status: 500, + }), + true, + ], + [ + new Response(null, { + statusText: "", + }), + new Response(null, { + statusText: "", + }), + true, + ], + [ + new Response(null, { + headers: { + a: "", + }, + }), + new Response(null, { + headers: { + a: "", + }, + }), + true, + ], + [ + new Response(null, { + headers: { + a: "test", + }, + }), + new Response(null, { + headers: { + a: "", + }, + }), + false, + ], + [ + new Response(null, { + statusText: "", + }), + new Response(null, { + statusText: "a", + }), + false, + ], + [ + new Response(null, { + status: 200, + }), + new Response(null, { + status: 201, + }), + false, + ], + [ + new Response(null, { + status: 300, + }), + new Response(), + false, + ], + [new Response("test"), new Response(), false], + [new Response(""), new Response(""), true], + [new Response("a"), new Response(""), true], + [new Response("a"), new Response("a"), true], + ]; + + Promise.all(table.map(([left, right, result]) => { + assertEquals(equalsResponse(left, right), result); + })); + }); + + it("should pass if strict mode", async () => { + const table: [Response, Response, boolean][] = [ + [new Response(""), new Response(""), true], + [new Response("a"), new Response("a"), true], + [new Response("test"), new Response(), false], + [new Response("a"), new Response(""), false], + ]; + + await Promise.all(table.map(async ([left, right, result]) => { + assertEquals(await equalsResponse(left, right, true), result); + })); + }); + + it("should throw error if strict mode and the response body has been read", async () => { + const response = new Response(""); + await response.text(); + + assert(response.bodyUsed); + assertThrows(() => equalsResponse(response, response, true)); + }); + + it("should not throw when the response body has used", async () => { + const res = new Response(""); + + await res.text(); + + assert(res.bodyUsed); + assertFalse(equalsResponse(res, new Response(""))); + }); + + it("should use cloned response", async () => { + const res = new Response(""); + + assert(equalsResponse(res, new Response(""))); + assertFalse(res.bodyUsed); + assertEquals(await res.text(), ""); + }); +}); diff --git a/is.ts b/is.ts new file mode 100644 index 0000000..9f13c37 --- /dev/null +++ b/is.ts @@ -0,0 +1,18 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. +// This module is browser compatible. + +/** Whether the input is `Response` or not. + * + * @example + * ```ts + * import { isResponse } from "https://deno.land/x/response_utils@$VERSION/is.ts"; + * import { assert, assertFalse } from "https://deno.land/std/testing/asserts.ts"; + * + * assert(isResponse(new Response())); + * assertFalse(isResponse({})); + * assertFalse(isResponse(null)); + * ``` + */ +export function isResponse(input: unknown): input is Response { + return input instanceof Response; +} diff --git a/is_test.ts b/is_test.ts new file mode 100644 index 0000000..3b0839e --- /dev/null +++ b/is_test.ts @@ -0,0 +1,36 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. + +import { isResponse } from "./is.ts"; +import { assert, assertFalse, describe, it } from "./_dev_deps.ts"; + +describe("isResponse", () => { + it("should return true", () => { + const table: unknown[] = [ + new Response(), + new Response(""), + ]; + + table.forEach((value) => { + assert(isResponse(value)); + }); + }); + + it("should return false", () => { + const table: unknown[] = [ + {}, + null, + undefined, + 0, + NaN, + new Request("http://localhost"), + "", + false, + true, + [], + ]; + + table.forEach((value) => { + assertFalse(isResponse(value)); + }); + }); +});