Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 88 additions & 76 deletions otohttp/templates/client.ts.plush
Original file line number Diff line number Diff line change
@@ -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<Json> {}

<%= 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<ClientResponse>;
};

/**
* Decode json to a zod schema
*/
export async function decodeJson<T extends { error?: string }>(
something: Json,
schema: Schema<T, ZodTypeDef, Json>
): Promise<APIResponse<T>> {
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<T> =
| {
success: false;
error: ZodError<Json> | { status: number; statusText: string; message: string };
}
| { success: true; data: Omit<T, 'error'> };

<%= 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.infer<ReturnType<typeof <%= object.Name %>Schema>>;


<% } %>


<%= 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