-
Notifications
You must be signed in to change notification settings - Fork 2
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
Labels
Comments
4 tasks
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
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
)The text was updated successfully, but these errors were encountered: