diff --git a/otohttp/templates/client.ts.plush b/otohttp/templates/client.ts.plush index 9c24d47..d8cdedb 100644 --- a/otohttp/templates/client.ts.plush +++ b/otohttp/templates/client.ts.plush @@ -1,87 +1,99 @@ // Code generated by oto; DO NOT EDIT. -// HeadersFunc allows you to mutate headers for each request. -// Useful for adding authorization into the client. -interface HeadersFunc { - (headers: Headers): void; -} +import { z } from 'zod'; +import type { Schema, ZodTypeDef, ZodError } from 'zod'; -// Client provides access to remote services. -export class Client { - // basepath is the path prefix for the requests. - // This may be a path, or an absolute URL. - public basepath: String = '/oto/' - // headers allows calling code to mutate the HTTP - // headers of the underlying HTTP requests. - public headers?: HeadersFunc -} +type JsonPrimitive = string | number | boolean | undefined; +export type Json = JsonPrimitive | JsonObject | JsonArray; +type JsonObject = { [member: string]: Json } | {}; +interface JsonArray extends ReadonlyArray {} -<%= for (service) in def.Services { %> -<%= format_comment_text(service.Comment) %>export class <%= service.Name %> { - constructor(readonly client: Client) {} - <%= for (method) in service.Methods { %> - <%= format_comment_text(method.Comment) %> async <%= method.NameLowerCamel %>(<%= camelize_down(method.InputObject.TSType) %>?: <%= method.InputObject.TSType %>, modifyHeaders?: HeadersFunc): Promise<<%= method.OutputObject.TSType %>> { - if (<%= camelize_down(method.InputObject.TSType) %> == null) { - <%= camelize_down(method.InputObject.TSType) %> = new <%= method.InputObject.TSType %>(); - } - const headers: Headers = new Headers(); - headers.set('Accept', 'application/json'); - headers.set('Content-Type', 'application/json'); - if (this.client.headers) { - await this.client.headers(headers); - } - if (modifyHeaders) { - await modifyHeaders(headers) - } - const response = await fetch(this.client.basepath + '<%= service.Name %>.<%= method.Name %>', { - method: 'POST', - headers: headers, - body: JSON.stringify(<%= camelize_down(method.InputObject.TSType) %>), - }) - if (response.status !== 200) { - throw new Error(`<%= service.Name %>.<%= method.Name %>: ${response.status} ${response.statusText}`); - } - return response.json().then((json) => { - if (json.error) { - throw new Error(json.error); - } - return new <%= method.OutputObject.TSType %>(json); - }) - } - <% } %> +export type ClientResponse = + | { + success: false; + error: { status: number; statusText: string; message: string }; + } + | { success: true; payload: Json }; + +export type Client = { + fetch: (path: string, payload: Json) => Promise; +}; + +/** + * Decode json to a zod schema + */ +export async function decodeJson( + something: Json, + schema: Schema +): Promise> { + const result = await schema.safeParseAsync(something); + if (result.success) { + const { error } = result.data; + if (error !== undefined && error?.length > 0) { + return { + success: false, + error: { + status: 400, + statusText: 'API error', + message: error, + }, + }; + } + } + return result; } -<% } %> + +export type APIResponse = + | { + success: false; + error: ZodError | { status: number; statusText: string; message: string }; + } + | { success: true; data: Omit }; <%= for (object) in def.Objects { %> -<%= format_comment_text(object.Comment) %>export class <%= object.Name %> { - constructor(data?: any) { - if (data) { - <%= for (field) in object.Fields { %> - <%= if (field.Type.IsObject) { %> - <%= if (field.Type.Multiple) { %> - if (data.<%= field.NameLowerCamel %>) { - this.<%= field.NameLowerCamel %> = [] - for (let i = 0; i < data.<%= field.NameLowerCamel %>.length; i++) { - this.<%= field.NameLowerCamel %>.push(new <%= field.Type.TSType %>(data.<%= field.NameLowerCamel %>[i])); - } - } - <% } else { %> - this.<%= field.NameLowerCamel %> = new <%= field.Type.TSType %>(data.<%= field.NameLowerCamel %>); - <% } %> - <% } else { %> - this.<%= field.NameLowerCamel %> = data.<%= field.NameLowerCamel %>; - <% } %> - <% } %> - } - } -<%= for (field) in object.Fields { %> - <%= format_comment_text(field.Comment) %> <%= field.NameLowerCamel %><%= if (field.Type.IsObject || field.Type.Multiple) { %>?<% } %>: <%= if (field.Type.IsObject) { %><%= field.Type.TSType %><%= if (field.Type.Multiple) { %>[]<% } %><% } else { %><%= field.Type.JSType %><%= if (field.Type.Multiple) { %>[]<% } %><%= if (!field.Type.Multiple) { %> = <%= field.Type.JSType %>Default<% } %><% } %>; +<%= format_comment_text(object.Comment) %>export function <%= object.Name %>Schema() { return z.object({ + <%= for (field) in object.Fields { %> + <%= field.NameLowerCamel %>: <%= if (field.Type.IsObject) { %> + <%= field.Type.TSType %>Schema() + <% } else if (field.Metadata["options"]) { %> +z.enum([<%= for (option) in field.Metadata["options"] { %>'<%= option %>',<% } %> ""]) + <% } else if (field.Type.JSType == "object") { %> + z.record(z.unknown()) + <% } else { %> z.<%= field.Type.JSType %>() <% } %><%= if (field.Type.Multiple) { %>.array()<% } %><%= if (field.Metadata["optional"]) {%>.optional()<%}%> ,<% } %> + }); +} + + export type <%= object.Name %> = z.inferSchema>>; + + <% } %> + + +<%= for (service) in def.Services { %> +<%= if (service.Metadata["deprecated"]) { %> + /** + * @deprecated <%= service.Metadata["deprecated"] %> + */ +<% } else { %><%= format_comment_text(service.Comment) } %>export class <%= service.Name %> { + private readonly _client: Client; + public constructor(client: Client) { + this._client = client; + } + <%= for (method) in service.Methods { %> + <%= if (method.Metadata["deprecated"]) { %> + /** + * @deprecated <%= method.Metadata["deprecated"] %> + */ + <% } else { %><%= format_comment_text(method.Comment) } %>public async <%= method.NameLowerCamel %>(<%= camelize_down(method.InputObject.TSType) %>: <%= method.InputObject.TSType %>): Promise< + APIResponse<<%= method.OutputObject.TSType %>> + > { + const response = await this._client.fetch('<%= service.Name %>.<%= method.Name %>', <%= camelize_down(method.InputObject.TSType) %>); + if (!response.success) { + return response; + } + return decodeJson(response.payload, <%= method.OutputObject.TSType %>Schema()); + } + <% } %> } <% } %> -// these defaults make the template easier to write. -const stringDefault = '' -const numberDefault = 0 -const booleanDefault = false -const anyDefault = null