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

typescript-fetch: support runtime parsing #82

Open
mnahkies opened this issue Sep 24, 2023 · 0 comments
Open

typescript-fetch: support runtime parsing #82

mnahkies opened this issue Sep 24, 2023 · 0 comments

Comments

@mnahkies
Copy link
Owner

currently only the typescript-koa does any runtime parsing/validation.

add support to the typescript-fetch template for validating parameters, and parsing responses using the configured schema builder (ie: zoa / joi)

mnahkies added a commit that referenced this issue Feb 10, 2024
…ion (#112)

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**

<details><summary>typescript-fetch</summary>
<p>

```diff
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(),
+})

```

</p>
</details> 

<details><summary>typescript-axios</summary>
<p>

```diff
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(),
+})
```

</p>
</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant