Repository: RobinTail/express-zod-api
Pull request #974 (2023-04-30 22:59:58)

Integration variant
Related to the discussion https://github.com/RobinTail/express-zod-api/discussions/931


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8215dd98..662f89d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 
 ## Version 10
 
+### v10.6.0
+
+- Feature #974: Integration variant.
+  - `Integration::constructor()` has gotten a new property `variant` with two possible values:
+    - `client` _(default)_ — the familiar entity for making typed requests and received typed responses;
+    - `types` — only types of your endpoint requests and responses (for making a DIY solution).
+  - The deprecated ~~`Client::constructor()`~~ implies `client` variant of `Integration`.
+
 ### v10.5.0
 
 - Errors that may occur when generating documentation are now more informative.
diff --git a/README.md b/README.md
index 6d5dc7d8..bfe539a5 100644
--- a/README.md
+++ b/README.md
@@ -815,6 +815,7 @@ fs.writeFileSync(
   "./frontend/client.ts",
   new Integration({
     routing,
+    variant: "client", // <— optional, see also "types" for a DIY solution
     optionalPropStyle: { withQuestionMark: true, withUndefined: true }, // optional
   }).print(),
   "utf-8"
diff --git a/src/integration.ts b/src/integration.ts
index 4ee9ac83..33583218 100644
--- a/src/integration.ts
+++ b/src/integration.ts
@@ -37,6 +37,13 @@ interface Registry {
 
 interface IntegrationParams {
   routing: Routing;
+  /**
+   * @desc What should be generated
+   * @example "types" — types of your endpoint requests and responses (for a DIY solution)
+   * @example "client" — an entity for performing typed requests and receiving typed responses
+   * @default "client"
+   * */
+  variant?: "types" | "client";
   /**
    * @desc Used for comparing schemas wrapped into z.lazy() to limit the recursion
    * @default JSON.stringify() + SHA1 hash as a hex digest
@@ -77,6 +84,7 @@ export class Integration {
 
   constructor({
     routing,
+    variant = "client",
     serializer = defaultSerializer,
     optionalPropStyle = { withQuestionMark: true, withUndefined: true },
   }: IntegrationParams) {
@@ -120,6 +128,10 @@ export class Integration {
 
     this.agg = Object.values<ts.Node>(this.aliases).concat(this.agg);
 
+    if (variant === "types") {
+      return;
+    }
+
     const pathNode = makePublicLiteralType("Path", this.paths);
     const methodNode = makePublicLiteralType("Method", methods);
 
@@ -327,4 +339,8 @@ export class Integration {
  * @deprecated Use Integration instead
  * @todo remove in v11
  * */
-export class Client extends Integration {}
+export class Client extends Integration {
+  constructor(params: Omit<IntegrationParams, "variant">) {
+    super({ variant: "client", ...params });
+  }
+}


Test patch:
diff --git a/tests/unit/__snapshots__/integration.spec.ts.snap b/tests/unit/__snapshots__/integration.spec.ts.snap
index 013a6a17..72545bc7 100644
--- a/tests/unit/__snapshots__/integration.spec.ts.snap
+++ b/tests/unit/__snapshots__/integration.spec.ts.snap
@@ -332,6 +332,88 @@ export class ExpressZodAPIClient {
 }"
 `;
 
+exports[`API Integration Generator Should generate a types for example API 1`] = `
+"type Type2048581c137c5b2130eb860e3ae37da196dfc25b = {
+    title: string;
+    features: Type2048581c137c5b2130eb860e3ae37da196dfc25b;
+}[];
+
+type GetV1UserRetrieveInput = {} & {
+    /** a numeric string containing the id of the user */
+    id: string;
+};
+
+type GetV1UserRetrieveResponse = {
+    status: "success";
+    data: {
+        id: number;
+        name: string;
+        features: {
+            title: string;
+            features: Type2048581c137c5b2130eb860e3ae37da196dfc25b;
+        }[];
+    };
+} | {
+    status: "error";
+    error: {
+        message: string;
+    };
+};
+
+type PostV1UserIdInput = {
+    key: string;
+} & {
+    id: string;
+    name: string;
+    birthday: string;
+};
+
+type PostV1UserIdResponse = {
+    status: "success";
+    data: {
+        name: string;
+        createdAt: string;
+    };
+} | {
+    status: "error";
+    error: {
+        message: string;
+    };
+};
+
+type GetV1AvatarSendInput = {
+    userId: string;
+};
+
+type GetV1AvatarSendResponse = string | string;
+
+type GetV1AvatarStreamInput = {
+    userId: string;
+};
+
+type GetV1AvatarStreamResponse = string | string;
+
+type PostV1AvatarUploadInput = {
+    avatar: any;
+};
+
+type PostV1AvatarUploadResponse = {
+    status: "success";
+    data: {
+        name: string;
+        size: number;
+        mime: string;
+        hash: string;
+        otherInputs: Record<string, any>;
+    };
+} | {
+    status: "error";
+    error: {
+        message: string;
+    };
+};"
+`;
+
 exports[`API Integration Generator Should treat optionals the same way as z.infer() by default 1`] = `
 "type PostV1TestWithDashesInput = {
     opt?: string | undefined;
diff --git a/tests/unit/integration.spec.ts b/tests/unit/integration.spec.ts
index 1166b4b4..0f340ba2 100644
--- a/tests/unit/integration.spec.ts
+++ b/tests/unit/integration.spec.ts
@@ -1,12 +1,15 @@
 import { z } from "zod";
 import { routing } from "../../example/routing";
-import { Integration, defaultEndpointsFactory } from "../../src";
+import { Client, Integration, defaultEndpointsFactory } from "../../src";
 
 describe("API Integration Generator", () => {
-  test("Should generate a client for example API", () => {
-    const client = new Integration({ routing });
-    expect(client.print()).toMatchSnapshot();
-  });
+  test.each(["client", "types"] as const)(
+    "Should generate a %s for example API",
+    (variant) => {
+      const client = new Integration({ variant, routing });
+      expect(client.print()).toMatchSnapshot();
+    }
+  );
 
   test("Should treat optionals the same way as z.infer() by default", () => {
     const client = new Integration({
@@ -52,3 +55,11 @@ describe("API Integration Generator", () => {
     }
   );
 });
+
+describe("Client generator", () => {
+  test("Should be the same as Integration with the client variant", () => {
+    const gauge = new Integration({ variant: "client", routing }).print();
+    const client = new Client({ routing });
+    expect(client.print()).toEqual(gauge);
+  });
+});