From 222c6936d775086fe204f5b6d59a52d5de919f97 Mon Sep 17 00:00:00 2001 From: Christopher Lord Date: Wed, 31 May 2023 14:25:27 -0600 Subject: [PATCH 1/3] add: typescript template example with zod for typechecking --- example/client.gen.ts | 100 ++++++++++++++++++++++++++++++++++++++++ example/client.ts.plush | 99 +++++++++++++++++++++++++++++++++++++++ example/generate.sh | 6 +++ 3 files changed, 205 insertions(+) create mode 100644 example/client.gen.ts create mode 100644 example/client.ts.plush diff --git a/example/client.gen.ts b/example/client.gen.ts new file mode 100644 index 0000000..269fc94 --- /dev/null +++ b/example/client.gen.ts @@ -0,0 +1,100 @@ +// Code generated by oto; DO NOT EDIT. + +import { z } from 'zod'; +import type { Schema, ZodTypeDef, ZodError } from 'zod'; + +type JsonPrimitive = string | number | boolean | undefined; +export type Json = JsonPrimitive | JsonObject | JsonArray; +type JsonObject = { [member: string]: Json } | {}; +interface JsonArray extends ReadonlyArray {} + +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 }; + + +// GreetRequest is the request object for GreeterService.Greet. +export function GreetRequestSchema() { return z.object({ + + name: z.string() , + }); +} + + export type GreetRequest = z.infer>; + + + +// GreetResponse is the response object containing a person's greeting. +export function GreetResponseSchema() { return z.object({ + + greeting: z.string() , + error: z.string() , + }); +} + + export type GreetResponse = z.infer>; + + + + + + +// GreeterService is a polite API for greeting people. +export class GreeterService { + private readonly _client: Client; + public constructor(client: Client) { + this._client = client; + } + + // Greet prepares a lovely greeting. +public async greet(greetRequest: GreetRequest): Promise< + APIResponse + > { + const response = await this._client.fetch('GreeterService.Greet', greetRequest); + if (!response.success) { + return response; + } + return decodeJson(response.payload, GreetResponseSchema()); + } + +} + + diff --git a/example/client.ts.plush b/example/client.ts.plush new file mode 100644 index 0000000..d8cdedb --- /dev/null +++ b/example/client.ts.plush @@ -0,0 +1,99 @@ +// Code generated by oto; DO NOT EDIT. + +import { z } from 'zod'; +import type { Schema, ZodTypeDef, ZodError } from 'zod'; + +type JsonPrimitive = string | number | boolean | undefined; +export type Json = JsonPrimitive | JsonObject | JsonArray; +type JsonObject = { [member: string]: Json } | {}; +interface JsonArray extends ReadonlyArray {} + +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 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()); + } + <% } %> +} +<% } %> + diff --git a/example/generate.sh b/example/generate.sh index 66f2751..c9108c0 100755 --- a/example/generate.sh +++ b/example/generate.sh @@ -13,6 +13,12 @@ oto -template client.js.plush \ ./def echo "generated client.gen.js" +oto -template client.ts.plush \ + -out client.gen.ts \ + -pkg main \ + ./def +echo "generated client.gen.ts" + oto -template client.swift.plush \ -out ./swift/SwiftCLIExample/SwiftCLIExample/client.gen.swift \ -pkg main \ From 1b6c441b4440356729f8777065a9303cfc198ad2 Mon Sep 17 00:00:00 2001 From: Christopher Lord Date: Thu, 1 Jun 2023 08:33:52 -0600 Subject: [PATCH 2/3] add client.ts to the otohttp/templates directory, too --- otohttp/templates/client.ts.plush | 164 ++++++++++++++++-------------- 1 file changed, 88 insertions(+), 76 deletions(-) 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 From 6a6c8f6341eda994707d9ac3d4c6bd191ff289fd Mon Sep 17 00:00:00 2001 From: Christopher Lord Date: Thu, 1 Jun 2023 09:12:33 -0600 Subject: [PATCH 3/3] remove example ts --- example/client.gen.ts | 100 ---------------------------------------- example/client.ts.plush | 99 --------------------------------------- example/generate.sh | 6 --- 3 files changed, 205 deletions(-) delete mode 100644 example/client.gen.ts delete mode 100644 example/client.ts.plush diff --git a/example/client.gen.ts b/example/client.gen.ts deleted file mode 100644 index 269fc94..0000000 --- a/example/client.gen.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Code generated by oto; DO NOT EDIT. - -import { z } from 'zod'; -import type { Schema, ZodTypeDef, ZodError } from 'zod'; - -type JsonPrimitive = string | number | boolean | undefined; -export type Json = JsonPrimitive | JsonObject | JsonArray; -type JsonObject = { [member: string]: Json } | {}; -interface JsonArray extends ReadonlyArray {} - -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 }; - - -// GreetRequest is the request object for GreeterService.Greet. -export function GreetRequestSchema() { return z.object({ - - name: z.string() , - }); -} - - export type GreetRequest = z.infer>; - - - -// GreetResponse is the response object containing a person's greeting. -export function GreetResponseSchema() { return z.object({ - - greeting: z.string() , - error: z.string() , - }); -} - - export type GreetResponse = z.infer>; - - - - - - -// GreeterService is a polite API for greeting people. -export class GreeterService { - private readonly _client: Client; - public constructor(client: Client) { - this._client = client; - } - - // Greet prepares a lovely greeting. -public async greet(greetRequest: GreetRequest): Promise< - APIResponse - > { - const response = await this._client.fetch('GreeterService.Greet', greetRequest); - if (!response.success) { - return response; - } - return decodeJson(response.payload, GreetResponseSchema()); - } - -} - - diff --git a/example/client.ts.plush b/example/client.ts.plush deleted file mode 100644 index d8cdedb..0000000 --- a/example/client.ts.plush +++ /dev/null @@ -1,99 +0,0 @@ -// Code generated by oto; DO NOT EDIT. - -import { z } from 'zod'; -import type { Schema, ZodTypeDef, ZodError } from 'zod'; - -type JsonPrimitive = string | number | boolean | undefined; -export type Json = JsonPrimitive | JsonObject | JsonArray; -type JsonObject = { [member: string]: Json } | {}; -interface JsonArray extends ReadonlyArray {} - -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 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()); - } - <% } %> -} -<% } %> - diff --git a/example/generate.sh b/example/generate.sh index c9108c0..66f2751 100755 --- a/example/generate.sh +++ b/example/generate.sh @@ -13,12 +13,6 @@ oto -template client.js.plush \ ./def echo "generated client.gen.js" -oto -template client.ts.plush \ - -out client.gen.ts \ - -pkg main \ - ./def -echo "generated client.gen.ts" - oto -template client.swift.plush \ -out ./swift/SwiftCLIExample/SwiftCLIExample/client.gen.swift \ -pkg main \