generated from well-known-components/base-ts-project
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* client codegen * client and server codegen
- Loading branch information
Showing
6 changed files
with
350 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { Message } from "google-protobuf" | ||
import { RpcClientPort } from "." | ||
|
||
export type Constructor<C> = { new (): C; deserializeBinary(data: Uint8Array): C } | ||
|
||
export function clientProcedureUnary<Ctor1 extends Message, Ctor2 extends Message>( | ||
port: unknown | Promise<unknown>, | ||
name: string, | ||
requestType: Constructor<Ctor1>, | ||
requestResponseConstructor: Constructor<Ctor2> | ||
) { | ||
const fn: (arg: Ctor1) => Promise<Ctor2> = async (arg: any): Promise<any> => { | ||
const remoteModule: Record<typeof name, (arg: Uint8Array) => Promise<any>> = (await port) as any | ||
|
||
if (!(arg instanceof requestType)) throw new Error("Argument passed to RPC Method " + name + " type mismatch.") | ||
if (!(name in remoteModule)) throw new Error("Method " + name + " not implemented in server port") | ||
|
||
const result = await remoteModule[name](arg.serializeBinary()) | ||
if (!result) { | ||
throw new Error("Server sent an empty or null response to method call " + name) | ||
} | ||
return requestResponseConstructor.deserializeBinary(result) | ||
} | ||
|
||
return fn | ||
} | ||
|
||
export function clientProcedureStream<Ctor1 extends Message, Ctor2 extends Message>( | ||
port: unknown | Promise<unknown>, | ||
name: string, | ||
requestType: Constructor<Ctor1>, | ||
requestResponseConstructor: Constructor<Ctor2> | ||
) { | ||
const fn: (arg: Ctor1) => AsyncGenerator<Ctor2> = async function* (arg: any) { | ||
const remoteModule: Record<typeof name, (arg: Uint8Array) => Promise<any>> = (await port) as any | ||
|
||
if (!(arg instanceof requestType)) throw new Error("Argument passed to RPC Method " + name + " type mismatch.") | ||
if (!(name in remoteModule)) throw new Error("Method " + name + " not implemented in server port") | ||
|
||
const result = await remoteModule[name](arg.serializeBinary()) | ||
if (!result) { | ||
throw new Error("Server sent an empty or null response to method call " + name) | ||
} | ||
for await (const bytes of await result) { | ||
yield requestResponseConstructor.deserializeBinary(bytes) | ||
} | ||
} | ||
|
||
return fn | ||
} | ||
|
||
export function serverProcedureUnary<Ctor1 extends Message, Ctor2 extends Message>( | ||
fn: (arg: Ctor1) => Promise<Ctor2>, | ||
name: string, | ||
ctor1: Constructor<Ctor1>, | ||
ctor2: Constructor<Ctor2> | ||
): (arg: Uint8Array) => Promise<Uint8Array> { | ||
return async function (argBinary) { | ||
const arg = ctor1.deserializeBinary(argBinary) | ||
const result = await fn(arg) | ||
|
||
if (!result) throw new Error("Empty or null responses are not allowed. Procedure: " + name) | ||
if (!(result instanceof ctor2)) | ||
throw new Error("Result of procedure " + name + " did not match the expected constructor") | ||
|
||
return result.serializeBinary() | ||
} | ||
} | ||
|
||
export function serverProcedureStream<Ctor1 extends Message, Ctor2 extends Message>( | ||
fn: (arg: Ctor1) => Promise<AsyncGenerator<Ctor2>> | AsyncGenerator<Ctor2>, | ||
name: string, | ||
ctor1: Constructor<Ctor1>, | ||
ctor2: Constructor<Ctor2> | ||
): (arg: Uint8Array) => AsyncGenerator<Uint8Array> { | ||
return async function* (argBinary) { | ||
const arg = ctor1.deserializeBinary(argBinary) | ||
const result = await fn(arg) | ||
|
||
if (!result) throw new Error("Empty or null responses are not allowed. Procedure: " + name) | ||
|
||
for await (const elem of result) { | ||
if (!(elem instanceof ctor2)) | ||
throw new Error("Yielded result of procedure " + name + " did not match the expected constructor") | ||
|
||
yield elem.serializeBinary() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { Message } from "google-protobuf" | ||
import { RpcClient, RpcClientPort } from "../src" | ||
import { clientProcedureStream, clientProcedureUnary } from "../src/codegen" | ||
import { Book, GetBookRequest, QueryBooksRequest } from "./codegen/client_pb" | ||
import { createSimpleTestEnvironment, takeAsync } from "./helpers" | ||
import { log } from "./logger" | ||
|
||
/// service BookService { | ||
export type BookService = { | ||
/// rpc GetBook(GetBookRequest) returns (Book) {} | ||
GetBook(arg: GetBookRequest): Promise<Book> | ||
/// rpc QueryBooks(QueryBooksRequest) returns (stream Book) {} | ||
QueryBooks(arg: QueryBooksRequest): AsyncGenerator<Book> | ||
} | ||
/// } | ||
|
||
const FAIL_WITH_EXCEPTION_ISBN = 1 | ||
|
||
export function loadBookService(port: RpcClientPort): BookService { | ||
const mod = port.loadModule('BookService') | ||
return { | ||
GetBook: clientProcedureUnary(mod, "GetBook", GetBookRequest, Book), | ||
QueryBooks: clientProcedureStream(mod, "QueryBooks", QueryBooksRequest, Book), | ||
} | ||
} | ||
|
||
describe("codegen client", () => { | ||
const testEnv = createSimpleTestEnvironment({ | ||
async initializePort(port) { | ||
port.registerModule("BookService", async (port) => ({ | ||
async GetBook(arg: Uint8Array) { | ||
const req = GetBookRequest.deserializeBinary(arg) | ||
|
||
if (req.getIsbn() == FAIL_WITH_EXCEPTION_ISBN) throw new Error("ErrorMessage") | ||
|
||
const book = new Book() | ||
book.setAuthor("menduz") | ||
book.setIsbn(req.getIsbn()) | ||
book.setTitle("Rpc onion layers") | ||
return book.serializeBinary() | ||
}, | ||
async *QueryBooks(arg: Uint8Array) { | ||
const req = QueryBooksRequest.deserializeBinary(arg) | ||
|
||
if (req.getAuthorPrefix() == "fail_before_yield") throw new Error("fail_before_yield") | ||
|
||
const books = [ | ||
{ author: "mr menduz", isbn: 1234, title: "1001 reasons to write your own OS" }, | ||
{ author: "mr cazala", isbn: 1111, title: "Advanced CSS" }, | ||
{ author: "mr mannakia", isbn: 7666, title: "Advanced binary packing" }, | ||
{ author: "mr kuruk", isbn: 7668, title: "Advanced bots AI" }, | ||
] | ||
|
||
for (const book of books) { | ||
if (book.author.includes(req.getAuthorPrefix())) { | ||
const protoBook = new Book() | ||
protoBook.setAuthor(book.author) | ||
protoBook.setIsbn(book.isbn) | ||
protoBook.setTitle(book.title) | ||
yield protoBook.serializeBinary() | ||
} | ||
} | ||
|
||
if (req.getAuthorPrefix() == "fail_before_end") throw new Error("fail_before_end") | ||
}, | ||
})) | ||
}, | ||
}) | ||
|
||
let service: BookService | ||
|
||
it("basic service wraper creation", async () => { | ||
const { rpcClient } = testEnv | ||
|
||
const clientPort = await rpcClient.createPort("test1") | ||
service = loadBookService(clientPort) | ||
}) | ||
|
||
it("calls an unary method", async () => { | ||
const req = new GetBookRequest() | ||
req.setIsbn(1234) | ||
const ret = await service.GetBook(req) | ||
expect(ret).toBeInstanceOf(Book) | ||
expect(ret.getIsbn()).toEqual(1234) | ||
expect(ret.getAuthor()).toEqual("menduz") | ||
}) | ||
|
||
it("calls a streaming method", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("mr") | ||
|
||
const results: Book[] = [] | ||
|
||
for await (const book of service.QueryBooks(req)) { | ||
expect(book).toBeInstanceOf(Book) | ||
expect(book.getAuthor()).toMatch(/^mr\s.+/) | ||
results.push(book) | ||
} | ||
|
||
expect(results).toHaveLength(4) | ||
}) | ||
|
||
it("calls to unary fails throws error in client", async () => { | ||
const req = new GetBookRequest() | ||
req.setIsbn(FAIL_WITH_EXCEPTION_ISBN) | ||
await expect(service.GetBook(req)).rejects.toThrowError("RemoteError: ErrorMessage") | ||
}) | ||
|
||
it("calls to streaming fails throws error in client, fail_before_yield", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("fail_before_yield") | ||
await expect(service.QueryBooks(req).next()).rejects.toThrowError("RemoteError: fail_before_yield") | ||
}) | ||
|
||
it("calls to streaming fails throws error in client, fail_before_end", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("fail_before_end") | ||
await expect(() => takeAsync(service.QueryBooks(req))).rejects.toThrowError("RemoteError: fail_before_end") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { RpcServerPort } from "../src" | ||
import { Book, GetBookRequest, QueryBooksRequest } from "./codegen/client_pb" | ||
import { createSimpleTestEnvironment, takeAsync } from "./helpers" | ||
import * as codegen from "../src/codegen" | ||
import { loadBookService } from "./codegen-client.spec" | ||
|
||
/// service BookService { | ||
export type BookService = { | ||
/// rpc GetBook(GetBookRequest) returns (Book) {} | ||
GetBook(arg: GetBookRequest): Promise<Book> | ||
/// rpc QueryBooks(QueryBooksRequest) returns (stream Book) {} | ||
QueryBooks(arg: QueryBooksRequest): AsyncGenerator<Book> | ||
} | ||
/// } | ||
|
||
const FAIL_WITH_EXCEPTION_ISBN = 1 | ||
|
||
export type BookServiceModuleInitializator = (port: RpcServerPort) => Promise<BookService> | ||
|
||
export function registerBookService(port: RpcServerPort, moduleInitializator: BookServiceModuleInitializator): void { | ||
port.registerModule("BookService", async (port) => { | ||
const mod = await moduleInitializator(port) | ||
return { | ||
GetBook: codegen.serverProcedureUnary(mod["GetBook"].bind(mod), "GetBook", GetBookRequest, Book), | ||
QueryBooks: codegen.serverProcedureStream(mod["QueryBooks"].bind(mod), "QueryBooks", QueryBooksRequest, Book), | ||
} | ||
}) | ||
} | ||
|
||
describe("codegen client & server", () => { | ||
const testEnv = createSimpleTestEnvironment({ | ||
async initializePort(port) { | ||
registerBookService(port, async () => ({ | ||
async GetBook(req: GetBookRequest) { | ||
if (req.getIsbn() == FAIL_WITH_EXCEPTION_ISBN) throw new Error("ErrorMessage") | ||
|
||
const book = new Book() | ||
book.setAuthor("menduz") | ||
book.setIsbn(req.getIsbn()) | ||
book.setTitle("Rpc onion layers") | ||
return book | ||
}, | ||
async *QueryBooks(req: QueryBooksRequest) { | ||
if (req.getAuthorPrefix() == "fail_before_yield") throw new Error("fail_before_yield") | ||
|
||
const books = [ | ||
{ author: "mr menduz", isbn: 1234, title: "1001 reasons to write your own OS" }, | ||
{ author: "mr cazala", isbn: 1111, title: "Advanced CSS" }, | ||
{ author: "mr mannakia", isbn: 7666, title: "Advanced binary packing" }, | ||
{ author: "mr kuruk", isbn: 7668, title: "Advanced bots AI" }, | ||
] | ||
|
||
for (const book of books) { | ||
if (book.author.includes(req.getAuthorPrefix())) { | ||
const protoBook = new Book() | ||
protoBook.setAuthor(book.author) | ||
protoBook.setIsbn(book.isbn) | ||
protoBook.setTitle(book.title) | ||
yield protoBook | ||
} | ||
} | ||
|
||
if (req.getAuthorPrefix() == "fail_before_end") throw new Error("fail_before_end") | ||
}, | ||
})) | ||
}, | ||
}) | ||
|
||
let service: BookService | ||
|
||
it("basic service wraper creation", async () => { | ||
const { rpcClient } = testEnv | ||
|
||
const clientPort = await rpcClient.createPort("test1") | ||
service = loadBookService(clientPort) | ||
}) | ||
|
||
it("calls an unary method", async () => { | ||
const req = new GetBookRequest() | ||
req.setIsbn(1234) | ||
const ret = await service.GetBook(req) | ||
expect(ret).toBeInstanceOf(Book) | ||
expect(ret.getIsbn()).toEqual(1234) | ||
expect(ret.getAuthor()).toEqual("menduz") | ||
}) | ||
|
||
it("calls a streaming method", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("mr") | ||
|
||
const results: Book[] = [] | ||
|
||
for await (const book of service.QueryBooks(req)) { | ||
expect(book).toBeInstanceOf(Book) | ||
expect(book.getAuthor()).toMatch(/^mr\s.+/) | ||
results.push(book) | ||
} | ||
|
||
expect(results).toHaveLength(4) | ||
}) | ||
|
||
it("calls to unary fails throws error in client", async () => { | ||
const req = new GetBookRequest() | ||
req.setIsbn(FAIL_WITH_EXCEPTION_ISBN) | ||
await expect(service.GetBook(req)).rejects.toThrowError("RemoteError: ErrorMessage") | ||
}) | ||
|
||
it("calls to streaming fails throws error in client, fail_before_yield", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("fail_before_yield") | ||
await expect(service.QueryBooks(req).next()).rejects.toThrowError("RemoteError: fail_before_yield") | ||
}) | ||
|
||
it("calls to streaming fails throws error in client, fail_before_end", async () => { | ||
const req = new QueryBooksRequest() | ||
req.setAuthorPrefix("fail_before_end") | ||
await expect(() => takeAsync(service.QueryBooks(req))).rejects.toThrowError("RemoteError: fail_before_end") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
syntax = "proto3"; | ||
|
||
message Book { | ||
int64 isbn = 1; | ||
string title = 2; | ||
string author = 3; | ||
} | ||
|
||
message GetBookRequest { | ||
int64 isbn = 1; | ||
} | ||
|
||
message QueryBooksRequest { | ||
string author_prefix = 1; | ||
} |