Repository: pmcelhaney/counterfact
Pull request #149 (2022-08-05 14:17:28)

133 response builder fluent api
- response builder object
- pass a response builder object in the request
- update the demo app to use the response object
- changeset for response builder API



Patch:
diff --git a/.changeset/early-cougars-argue.md b/.changeset/early-cougars-argue.md
new file mode 100644
index 0000000..6fbb393
--- /dev/null
+++ b/.changeset/early-cougars-argue.md
@@ -0,0 +1,5 @@
+---
+"counterfact": minor
+---
+
+response builder fluent API which is going to make intellisense with TypeScript awesome
diff --git a/demo/paths/hello/{name}.js b/demo/paths/hello/{name}.js
index ec3a01f..7ee8723 100644
--- a/demo/paths/hello/{name}.js
+++ b/demo/paths/hello/{name}.js
@@ -1,13 +1,7 @@
-export function GET({ path, context, query }) {
+export function GET({ path, context, query, response }) {
   context.visits ??= {};
   context.visits[path.name] ??= 0;
   context.visits[path.name] += 1;
 
-  if (!path) {
-    return { body: "Hello, stranger!" };
-  }
-
-  return {
-    body: `${query.greeting ?? "Hello"}, ${path.name}!`,
-  };
+  return response["200"].text(`${query.greeting ?? "Hello!!!"}, ${path.name}!`);
 }
diff --git a/src/dispatcher.js b/src/dispatcher.js
index 17323ef..21e7663 100644
--- a/src/dispatcher.js
+++ b/src/dispatcher.js
@@ -1,5 +1,6 @@
 import Accept from "@hapi/accept";
 
+import { createResponseBuilder } from "./response-builder.js";
 import { Tools } from "./tools.js";
 
 export class Dispatcher {
@@ -21,6 +22,7 @@ export class Dispatcher {
         body,
         query,
         headers,
+        response: createResponseBuilder(),
       }),
       headers?.accept ?? "*/*"
     );
diff --git a/src/response-builder.js b/src/response-builder.js
new file mode 100644
index 0000000..229c3c8
--- /dev/null
+++ b/src/response-builder.js
@@ -0,0 +1,47 @@
+export function createResponseBuilder() {
+  return new Proxy(
+    {},
+    {
+      get: (target, name) => ({
+        status: Number.parseInt(name, 10),
+
+        header(name, value) {
+          return {
+            ...this,
+
+            headers: {
+              ...this.headers,
+              [name]: value,
+            },
+          };
+        },
+
+        match(contentType, body) {
+          return {
+            ...this,
+
+            content: [
+              ...(this.content ?? []),
+              {
+                body,
+                type: contentType,
+              },
+            ],
+          };
+        },
+
+        text(body) {
+          return this.match("text/plain", body);
+        },
+
+        html(body) {
+          return this.match("text/html", body);
+        },
+
+        json(body) {
+          return this.match("application/json", body);
+        },
+      }),
+    }
+  );
+}


Test patch:
diff --git a/test/dispatcher.test.js b/test/dispatcher.test.js
index 1f16532..31f7f6c 100644
--- a/test/dispatcher.test.js
+++ b/test/dispatcher.test.js
@@ -264,6 +264,28 @@ describe("a dispatcher", () => {
     expect(textResponse.body).toBe(false);
   });
 
+  it("passes a response builder", async () => {
+    const registry = new Registry();
+
+    registry.add("/a", {
+      GET({ response }) {
+        return response[200].text("hello").html("<h1>hello</h1>");
+      },
+    });
+
+    const dispatcher = new Dispatcher(registry);
+    const htmlResponse = await dispatcher.request({
+      method: "GET",
+      path: "/a",
+
+      headers: {
+        accept: "text/html",
+      },
+    });
+
+    expect(htmlResponse.body).toBe("<h1>hello</h1>");
+  });
+
   it("passes status code in the response", async () => {
     const registry = new Registry();
 
diff --git a/test/response-builder.test.js b/test/response-builder.test.js
new file mode 100644
index 0000000..e2dca06
--- /dev/null
+++ b/test/response-builder.test.js
@@ -0,0 +1,48 @@
+import { createResponseBuilder } from "../src/response-builder.js";
+
+describe("a response builder", () => {
+  it("starts building a response object when the status is selected", () => {
+    const response = createResponseBuilder();
+
+    expect(response[200].status).toBe(200);
+    expect(response["404 NOT FOUND"].status).toBe(404);
+  });
+
+  it("adds a content type and body with match()", () => {
+    const response = createResponseBuilder()[200]
+      .match("text/plain", "hello")
+      .match("application/json", { hello: "world" });
+
+    expect(response.status).toBe(200);
+    expect(response.content).toStrictEqual([
+      { type: "text/plain", body: "hello" },
+      { type: "application/json", body: { hello: "world" } },
+    ]);
+  });
+
+  it("has shortcuts for json, text, and html", () => {
+    const response = createResponseBuilder()[200]
+      .text("hello")
+      .json({ hello: "world" })
+      .html("<h1>Hello World</h1>");
+
+    expect(response.status).toBe(200);
+    expect(response.content).toStrictEqual([
+      { type: "text/plain", body: "hello" },
+      { type: "application/json", body: { hello: "world" } },
+      { type: "text/html", body: "<h1>Hello World</h1>" },
+    ]);
+  });
+
+  it("adds headers", () => {
+    const response = createResponseBuilder()[200]
+      .header("x-one", "one")
+      .header("x-two", 2);
+
+    expect(response.status).toBe(200);
+    expect(response.headers).toStrictEqual({
+      "x-one": "one",
+      "x-two": 2,
+    });
+  });
+});