Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client generators support experimental runtime response validation #112

Merged
merged 8 commits into from
Feb 10, 2024

Conversation

mnahkies
Copy link
Owner

@mnahkies mnahkies commented Nov 29, 2023

Adds support for (optional) runtime response validation using zod for the typescript-fetch and typescript-axios client builders, enabled using the --enable-runtime-response-validation cli flag.

Currently considered experimental and probably buggy.

Additionally fixed some incorrect return types for the typescript-axios template, and made empty schema files stop being output.

Partially implements #82

TODO
Probably not to be completed in this PR, but these are roughly the outstanding actions before it would be considered stable

  • Support joi
  • Support typescript-angular
  • Documentation
  • Further testing

Example output

typescript-fetch

diff --git a/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts
index e441863..4e07356 100644
--- a/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts
+++ b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts
@@ -3,6 +3,7 @@
 /* eslint-disable */
 
 import { t_CreateUpdateTodoList, t_Error, t_TodoList } from "./models"
+import { s_Error, s_TodoList } from "./schemas"
 import {
   AbstractFetchClient,
   AbstractFetchClientConfig,
@@ -14,6 +15,7 @@ import {
   StatusCode5xx,
   TypedFetchResponse,
 } from "@nahkies/typescript-fetch-runtime/main"
+import { responseValidationFactory } from "@nahkies/typescript-fetch-runtime/zod"
 import { z } from "zod"
 
 export interface ApiClientConfig extends AbstractFetchClientConfig {}
@@ -34,7 +36,16 @@ export class ApiClient extends AbstractFetchClient {
     const url = this.basePath + `/list`
     const query = this._query({ created: p["created"], status: p["status"] })
 
-    return this._fetch(url + query, { method: "GET", ...(opts ?? {}) }, timeout)
+    const res = this._fetch(
+      url + query,
+      { method: "GET", ...(opts ?? {}) },
+      timeout,
+    )
+
+    return responseValidationFactory(
+      [["200", z.array(s_TodoList)]],
+      undefined,
+    )(res)
   }
 
   async getTodoListById(
@@ -50,7 +61,15 @@ export class ApiClient extends AbstractFetchClient {
   > {
     const url = this.basePath + `/list/${p["listId"]}`
 
-    return this._fetch(url, { method: "GET", ...(opts ?? {}) }, timeout)
+    const res = this._fetch(url, { method: "GET", ...(opts ?? {}) }, timeout)
+
+    return responseValidationFactory(
+      [
+        ["200", s_TodoList],
+        ["4XX", s_Error],
+      ],
+      z.undefined(),
+    )(res)
   }
 
   async updateTodoListById(
@@ -69,11 +88,19 @@ export class ApiClient extends AbstractFetchClient {
     const headers = this._headers({ "Content-Type": "application/json" })
     const body = JSON.stringify(p.requestBody)
 
-    return this._fetch(
+    const res = this._fetch(
       url,
       { method: "PUT", headers, body, ...(opts ?? {}) },
       timeout,
     )
+
+    return responseValidationFactory(
+      [
+        ["200", s_TodoList],
+        ["4XX", s_Error],
+      ],
+      z.undefined(),
+    )(res)
   }
 
   async deleteTodoListById(
@@ -89,7 +116,15 @@ export class ApiClient extends AbstractFetchClient {
   > {
     const url = this.basePath + `/list/${p["listId"]}`
 
-    return this._fetch(url, { method: "DELETE", ...(opts ?? {}) }, timeout)
+    const res = this._fetch(url, { method: "DELETE", ...(opts ?? {}) }, timeout)
+
+    return responseValidationFactory(
+      [
+        ["204", z.undefined()],
+        ["4XX", s_Error],
+      ],
+      z.undefined(),
+    )(res)
   }
 
   async getTodoListItems(
@@ -120,7 +155,23 @@ export class ApiClient extends AbstractFetchClient {
   > {
     const url = this.basePath + `/list/${p["listId"]}/items`
 
-    return this._fetch(url, { method: "GET", ...(opts ?? {}) }, timeout)
+    const res = this._fetch(url, { method: "GET", ...(opts ?? {}) }, timeout)
+
+    return responseValidationFactory(
+      [
+        [
+          "200",
+          z.object({
+            id: z.string(),
+            content: z.string(),
+            createdAt: z.string().datetime({ offset: true }),
+            completedAt: z.string().datetime({ offset: true }).optional(),
+          }),
+        ],
+        ["5XX", z.object({ message: z.string(), code: z.string() })],
+      ],
+      undefined,
+    )(res)
   }
 
   async createTodoListItem(
@@ -139,10 +190,12 @@ export class ApiClient extends AbstractFetchClient {
     const headers = this._headers({ "Content-Type": "application/json" })
     const body = JSON.stringify(p.requestBody)
 
-    return this._fetch(
+    const res = this._fetch(
       url,
       { method: "POST", headers, body, ...(opts ?? {}) },
       timeout,
     )
+
+    return responseValidationFactory([["204", z.undefined()]], undefined)(res)
   }
 }
diff --git a/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/schemas.ts b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/schemas.ts
new file mode 100644
index 0000000..4a8cbea
--- /dev/null
+++ b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/schemas.ts
@@ -0,0 +1,19 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import { z } from "zod"
+
+export const s_TodoList = z.object({
+  id: z.string(),
+  name: z.string(),
+  totalItemCount: z.coerce.number(),
+  incompleteItemCount: z.coerce.number(),
+  created: z.string().datetime({ offset: true }),
+  updated: z.string().datetime({ offset: true }),
+})
+
+export const s_Error = z.object({
+  message: z.string().optional(),
+  code: z.coerce.number().optional(),
+})

typescript-axios

diff --git a/integration-tests/typescript-axios/src/generated/todo-lists.yaml/client.ts b/integration-tests/typescript-axios/src/generated/todo-lists.yaml/client.ts
index 734179c..f5e4556 100644
--- a/integration-tests/typescript-axios/src/generated/todo-lists.yaml/client.ts
+++ b/integration-tests/typescript-axios/src/generated/todo-lists.yaml/client.ts
@@ -3,6 +3,7 @@
 /* eslint-disable */
 
 import { t_CreateUpdateTodoList, t_Error, t_TodoList } from "./models"
+import { s_Error, s_TodoList } from "./schemas"
 import {
   AbstractAxiosClient,
   AbstractAxiosConfig,
@@ -26,13 +27,15 @@ export class ApiClient extends AbstractAxiosClient {
     const url = `/list`
     const query = this._query({ created: p["created"], status: p["status"] })
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url + query,
       baseURL: this.basePath,
       method: "GET",
       timeout,
       ...(opts ?? {}),
     })
+
+    return { ...res, data: z.array(s_TodoList).parse(res.data) }
   }
 
   async getTodoListById(
@@ -44,13 +47,15 @@ export class ApiClient extends AbstractAxiosClient {
   ): Promise<AxiosResponse<t_TodoList>> {
     const url = `/list/${p["listId"]}`
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url,
       baseURL: this.basePath,
       method: "GET",
       timeout,
       ...(opts ?? {}),
     })
+
+    return { ...res, data: s_TodoList.parse(res.data) }
   }
 
   async updateTodoListById(
@@ -65,7 +70,7 @@ export class ApiClient extends AbstractAxiosClient {
     const headers = this._headers({ "Content-Type": "application/json" })
     const body = JSON.stringify(p.requestBody)
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url,
       baseURL: this.basePath,
       method: "PUT",
@@ -74,6 +79,8 @@ export class ApiClient extends AbstractAxiosClient {
       timeout,
       ...(opts ?? {}),
     })
+
+    return { ...res, data: s_TodoList.parse(res.data) }
   }
 
   async deleteTodoListById(
@@ -85,13 +92,15 @@ export class ApiClient extends AbstractAxiosClient {
   ): Promise<AxiosResponse<void>> {
     const url = `/list/${p["listId"]}`
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url,
       baseURL: this.basePath,
       method: "DELETE",
       timeout,
       ...(opts ?? {}),
     })
+
+    return { ...res, data: z.undefined().parse(res.data) }
   }
 
   async getTodoListItems(
@@ -110,13 +119,25 @@ export class ApiClient extends AbstractAxiosClient {
   > {
     const url = `/list/${p["listId"]}/items`
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url,
       baseURL: this.basePath,
       method: "GET",
       timeout,
       ...(opts ?? {}),
     })
+
+    return {
+      ...res,
+      data: z
+        .object({
+          id: z.string(),
+          content: z.string(),
+          createdAt: z.string().datetime({ offset: true }),
+          completedAt: z.string().datetime({ offset: true }).optional(),
+        })
+        .parse(res.data),
+    }
   }
 
   async createTodoListItem(
@@ -135,7 +156,7 @@ export class ApiClient extends AbstractAxiosClient {
     const headers = this._headers({ "Content-Type": "application/json" })
     const body = JSON.stringify(p.requestBody)
 
-    return this.axios.request({
+    const res = await this.axios.request({
       url: url,
       baseURL: this.basePath,
       method: "POST",
@@ -144,5 +165,7 @@ export class ApiClient extends AbstractAxiosClient {
       timeout,
       ...(opts ?? {}),
     })
+
+    return { ...res, data: z.undefined().parse(res.data) }
   }
 }
diff --git a/integration-tests/typescript-axios/src/generated/todo-lists.yaml/schemas.ts b/integration-tests/typescript-axios/src/generated/todo-lists.yaml/schemas.ts
new file mode 100644
index 0000000..4a8cbea
--- /dev/null
+++ b/integration-tests/typescript-axios/src/generated/todo-lists.yaml/schemas.ts
@@ -0,0 +1,19 @@
+/** AUTOGENERATED - DO NOT EDIT **/
+/* tslint:disable */
+/* eslint-disable */
+
+import { z } from "zod"
+
+export const s_TodoList = z.object({
+  id: z.string(),
+  name: z.string(),
+  totalItemCount: z.coerce.number(),
+  incompleteItemCount: z.coerce.number(),
+  created: z.string().datetime({ offset: true }),
+  updated: z.string().datetime({ offset: true }),
+})
+
+export const s_Error = z.object({
+  message: z.string().optional(),
+  code: z.coerce.number().optional(),
+})

@mnahkies mnahkies marked this pull request as draft November 29, 2023 20:21
mnahkies added a commit that referenced this pull request Dec 2, 2023
this will allow them to be used in client templates (eg: #112)
mnahkies added a commit that referenced this pull request Dec 2, 2023
to later be used for use-cases like #112 

unfortunately it's not smart enough to only output schemas that are
actually being used so creates a bit of noise at the moment.
@mnahkies mnahkies force-pushed the mn/feat/typescript-axios branch 2 times, most recently from 2ceb67d to 88ff2b1 Compare December 2, 2023 09:24
Base automatically changed from mn/feat/typescript-axios to main December 2, 2023 09:27
@mnahkies mnahkies force-pushed the mn/feat/typescript-axios-response-validation branch from c05c422 to 5e654c8 Compare December 2, 2023 09:32
@mnahkies mnahkies force-pushed the mn/feat/typescript-axios-response-validation branch 3 times, most recently from e0706b5 to 2a4ac10 Compare February 10, 2024 14:04
@mnahkies mnahkies force-pushed the mn/feat/typescript-axios-response-validation branch from 97371a2 to 4d6e899 Compare February 10, 2024 14:11
@mnahkies mnahkies marked this pull request as ready for review February 10, 2024 14:27
@mnahkies mnahkies changed the title feat: client generators support runtime response validation feat: client generators support experimental runtime response validation Feb 10, 2024
@mnahkies mnahkies enabled auto-merge (squash) February 10, 2024 14:30
@mnahkies mnahkies merged commit f3c3610 into main Feb 10, 2024
1 check passed
@mnahkies mnahkies deleted the mn/feat/typescript-axios-response-validation branch February 10, 2024 14:31
mnahkies added a commit that referenced this pull request Feb 17, 2024
follow up to #112 that adds `joi` support for client response validation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant