Skip to content

Commit

Permalink
Typescript multiple apis (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
wilmveel authored Nov 27, 2024
1 parent e72c8e7 commit 470e464
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 49 deletions.
31 changes: 25 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

jobs:

build-all:
build:

runs-on: macos-latest

Expand All @@ -34,7 +34,6 @@ jobs:
- name: Run
run: |
make build
make example
- name: Archive linux cli
uses: actions/upload-artifact@v3
with:
Expand All @@ -61,14 +60,36 @@ jobs:
name: wirespec-intellij-plugin
path: src/ide/intellij-plugin/build/distributions/intellij-plugin-*.zip

example:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: GPG_PASSPHRASE
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Run
run: |
make example
version:

runs-on: ubuntu-latest

if: github.event_name == 'release' && github.event.action == 'created'

needs:
- build-all
- build
- example

outputs:
version: ${{steps.version.outputs.version}}
Expand Down Expand Up @@ -204,9 +225,7 @@ jobs:
:src:plugin:gradle:publish \
:src:plugin:maven:publish \
:src:plugin:arguments:publish \
:src:integration:wirespec:publish \
:src:integration:jackson:publish \
:src:integration:spring:publish \
:src:tools:generator:publish
release-lib-npm:
Expand Down Expand Up @@ -239,4 +258,4 @@ jobs:
- name: Build
working-directory: ./src/plugin/npm/build/dist/js/productionLibrary
run: |
npm publish --access public
npm publish --access public
21 changes: 12 additions & 9 deletions examples/npm-typescript/src/clientProxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GetTodoById, GetTodos, PostTodo, Wirespec } from "./gen/Todo";
import {GetTodoById, GetTodos, PostTodo, Wirespec} from "./gen/Todo";
import {GetUsers} from "./gen/User";
import * as assert from "node:assert";
import Client = Wirespec.Client;

const serialization: Wirespec.Serialization = {
deserialize<T>(raw: string | undefined): T {
Expand Down Expand Up @@ -38,15 +38,17 @@ const mocks = [
mock("POST", ["api", "todos"], 200, {}, JSON.stringify({ id: "3", name: "Do more", done: true }))
];

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type Api<Han extends any> = Wirespec.Api<Wirespec.Request<any>, Wirespec.Response<any>, Han>
type WebClient = <Apis extends Api<any>[]>(...apis: Apis) => UnionToIntersection<Apis[number] extends Api<infer Han> ? Han : never>;
type ApiClient<REQ, RES> = (req: REQ) => Promise<RES>;
type WebClient = <Apis extends Wirespec.Api<Wirespec.Request<unknown>, Wirespec.Response<unknown>>[]>(...apis: Apis) => {
[K in Apis[number]['name']]: Extract<Apis[number], { name: K }> extends Wirespec.Api<infer Req, infer Res> ?
ApiClient<Req, Res> : never
};

const webClient:WebClient = (...apis) => {
const activeClients:Record<string, ReturnType<Client<Wirespec.Request<any>, Wirespec.Response<any>, any>>> = apis.reduce((acc, cur) => ({...acc, [cur.name] : cur.client(serialization)}), {})
const proxy = new Proxy({}, {
get: (_, prop) => {
const client = activeClients[prop as keyof typeof activeClients];
const api = apis.find(it => it.name === prop);
const client = api.client(serialization);
return (req:Wirespec.Request<unknown>) => {
const rawRequest = client.to(req);
const rawResponse: Wirespec.RawResponse = mocks.find(it =>
Expand All @@ -56,12 +58,13 @@ const webClient:WebClient = (...apis) => {
assert.notEqual(rawResponse, undefined);
return Promise.resolve(client.from(rawResponse));
}

},
});
return proxy as ReturnType<WebClient>
return proxy as any
}

const api = webClient(PostTodo.api, GetTodos.api, GetTodoById.api)
const api = webClient(PostTodo.api, GetTodos.api, GetTodoById.api, GetUsers.api)

const testGetTodos = async () => {
const request: GetTodos.Request = GetTodos.request();
Expand Down
2 changes: 1 addition & 1 deletion examples/npm-typescript/src/clientSimple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Api =
GetTodoById.Handler &
PostTodo.Handler

const handleFetch = <Req extends Wirespec.Request<any>, Res extends Wirespec.Response<any>>(client: Wirespec.Client<Req, Res, unknown>) => (request: Req): Promise<Res> => {
const handleFetch = <Req extends Wirespec.Request<any>, Res extends Wirespec.Response<any>>(client: Wirespec.Client<Req, Res>) => (request: Req): Promise<Res> => {
const mock = (method: Wirespec.Method, path: string[], status: number, headers: Record<string, string>, body: any) => ({
method,
path,
Expand Down
13 changes: 13 additions & 0 deletions examples/npm-typescript/wirespec/user.ws
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type User {
name: String
}

type Error {
code: Integer,
description: String
}

endpoint GetUsers GET /api/users -> {
200 -> User[]
}

19 changes: 9 additions & 10 deletions scripts/example.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env bash

dir="$(dirname -- "$0")"

./gradlew src:compiler:core:publishToMavenLocal &&
./gradlew src:integration:wirespec:publishToMavenLocal &&
./gradlew src:integration:jackson:publishToMavenLocal &&
./gradlew src:integration:spring:publishToMavenLocal &&
./gradlew jvmTest &&
./gradlew src:plugin:gradle:publishToMavenLocal &&
./gradlew src:plugin:maven:publishToMavenLocal &&
./gradlew \
src:converter:openapi:jvmJar \
src:plugin:arguments:jvmJar \
src:integration:jackson:publishToMavenLocal \
src:integration:spring:publishToMavenLocal \
src:plugin:gradle:publishToMavenLocal \
src:plugin:maven:publishToMavenLocal \
src:plugin:npm:jsNodeProductionLibraryDistribution &&
(cd "$dir"/../src/ide/vscode && npm i && npm run build) &&
(cd "$dir"/../examples && make clean && make build)
(cd "$dir"/../examples && make clean && make build)
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,23 @@ open class TypeScriptEmitter(logger: Logger = noLogger) : DefinitionModelEmitter
|${Spacer}}
|${endpoint.emitClient().prependIndent(Spacer(1))}
|${endpoint.emitServer().prependIndent(Spacer(1))}
|${Spacer}export const api: Wirespec.Api<Request, Response, Handler> = {
|${Spacer}export const api = {
|${Spacer(2)}name: "${endpoint.identifier.sanitizeSymbol().firstToLower()}",
|${Spacer(2)}method: "${endpoint.method.name}",
|${Spacer(2)}path: "${endpoint.path.joinToString("/") { it.emit() }}",
|${Spacer(2)}server,
|${Spacer(2)}client
|${Spacer}}
|${Spacer}} as const
|}
|
""".trimMargin()

override fun Endpoint.Segment.emit() =
when (this) {
is Endpoint.Segment.Literal -> value
is Endpoint.Segment.Param -> ":${identifier.value}"
}

private fun emitHandleFunction(endpoint: Endpoint) =
"${endpoint.identifier.sanitizeSymbol().firstToLower()}: (request:Request) => Promise<Response>"

Expand Down Expand Up @@ -187,7 +193,7 @@ open class TypeScriptEmitter(logger: Logger = noLogger) : DefinitionModelEmitter
.joinToString("")

private fun Endpoint.emitClient() = """
|export const client: Wirespec.Client<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
|export const client: Wirespec.Client<Request, Response> = (serialization: Wirespec.Serialization) => ({
|${emitClientTo().prependIndent(Spacer(1))},
|${emitClientFrom().prependIndent(Spacer(1))}
|})
Expand Down Expand Up @@ -230,7 +236,7 @@ open class TypeScriptEmitter(logger: Logger = noLogger) : DefinitionModelEmitter
""".trimMargin()

private fun Endpoint.emitServer() = """
|export const server:Wirespec.Server<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
|export const server:Wirespec.Server<Request, Response> = (serialization: Wirespec.Serialization) => ({
|${emitServerFrom().prependIndent(Spacer(1))},
|${emitServerTo().prependIndent(Spacer(1))}
|})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ abstract class Emitter(
}
}

fun Endpoint.Segment.emit() =
open fun Endpoint.Segment.emit() =
when (this) {
is Endpoint.Segment.Literal -> value
is Endpoint.Segment.Param -> "{${identifier.value}}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ data object TypeScriptShared : Shared {
|${Spacer}export type Request<T> = { path: Record<string, unknown>, method: Method, query?: Record<string, unknown>, headers?: Record<string, unknown>, content?:Content<T> }
|${Spacer}export type Response<T> = { status:number, headers?: Record<string, unknown[]>, content?:Content<T> }
|${Spacer}export type Serialization = { serialize: <T>(type: T) => string; deserialize: <T>(raw: string | undefined) => T }
|${Spacer}export type Client<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
|${Spacer}export type Server<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
|${Spacer}export type Api<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = { name: string; method: Method, path: string, client: Client<REQ, RES, HAN>; server: Server<REQ, RES, HAN> }
|${Spacer}export type Client<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
|${Spacer}export type Server<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
|${Spacer}export type Api<REQ extends Request<unknown>, RES extends Response<unknown>> = { name: string; method: Method, path: string, client: Client<REQ, RES>; server: Server<REQ, RES> }
|}
""".trimMargin()
}
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@ class CompileFullEndpointTest {
| export type Request<T> = { path: Record<string, unknown>, method: Method, query?: Record<string, unknown>, headers?: Record<string, unknown>, content?:Content<T> }
| export type Response<T> = { status:number, headers?: Record<string, unknown[]>, content?:Content<T> }
| export type Serialization = { serialize: <T>(type: T) => string; deserialize: <T>(raw: string | undefined) => T }
| export type Client<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
| export type Server<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
| export type Api<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = { name: string; method: Method, path: string, client: Client<REQ, RES, HAN>; server: Server<REQ, RES, HAN> }
| export type Client<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
| export type Server<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
| export type Api<REQ extends Request<unknown>, RES extends Response<unknown>> = { name: string; method: Method, path: string, client: Client<REQ, RES>; server: Server<REQ, RES> }
|}
|export namespace PutTodo {
| type Path = {
Expand Down Expand Up @@ -392,7 +392,7 @@ class CompileFullEndpointTest {
| export type Handler = {
| putTodo: (request:Request) => Promise<Response>
| }
| export const client: Wirespec.Client<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
| export const client: Wirespec.Client<Request, Response> = (serialization: Wirespec.Serialization) => ({
| to: (request) => ({
| method: "PUT",
| path: ["todos", serialization.serialize(request.path.id)],
Expand All @@ -419,7 +419,7 @@ class CompileFullEndpointTest {
| }
| }
| })
| export const server:Wirespec.Server<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
| export const server:Wirespec.Server<Request, Response> = (serialization: Wirespec.Serialization) => ({
| from: (request) => {
| return {
| method: "PUT",
Expand All @@ -441,13 +441,13 @@ class CompileFullEndpointTest {
| body: serialization.serialize(response.body),
| })
| })
| export const api: Wirespec.Api<Request, Response, Handler> = {
| export const api = {
| name: "putTodo",
| method: "PUT",
| path: "todos/{id}",
| path: "todos/:id",
| server,
| client
| }
| } as const
|}
|
|export type PotentialTodoDto = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ class CompileMinimalEndpointTest {
| export type Request<T> = { path: Record<string, unknown>, method: Method, query?: Record<string, unknown>, headers?: Record<string, unknown>, content?:Content<T> }
| export type Response<T> = { status:number, headers?: Record<string, unknown[]>, content?:Content<T> }
| export type Serialization = { serialize: <T>(type: T) => string; deserialize: <T>(raw: string | undefined) => T }
| export type Client<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
| export type Server<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
| export type Api<REQ extends Request<unknown>, RES extends Response<unknown>, HAN> = { name: string; method: Method, path: string, client: Client<REQ, RES, HAN>; server: Server<REQ, RES, HAN> }
| export type Client<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { to: (request: REQ) => RawRequest; from: (response: RawResponse) => RES }
| export type Server<REQ extends Request<unknown>, RES extends Response<unknown>> = (serialization: Serialization) => { from: (request: RawRequest) => REQ; to: (response: RES) => RawResponse }
| export type Api<REQ extends Request<unknown>, RES extends Response<unknown>> = { name: string; method: Method, path: string, client: Client<REQ, RES>; server: Server<REQ, RES> }
|}
|export namespace GetTodos {
| type Path = {}
Expand Down Expand Up @@ -274,7 +274,7 @@ class CompileMinimalEndpointTest {
| export type Handler = {
| getTodos: (request:Request) => Promise<Response>
| }
| export const client: Wirespec.Client<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
| export const client: Wirespec.Client<Request, Response> = (serialization: Wirespec.Serialization) => ({
| to: (request) => ({
| method: "GET",
| path: ["todos"],
Expand All @@ -295,7 +295,7 @@ class CompileMinimalEndpointTest {
| }
| }
| })
| export const server:Wirespec.Server<Request, Response, Handler> = (serialization: Wirespec.Serialization) => ({
| export const server:Wirespec.Server<Request, Response> = (serialization: Wirespec.Serialization) => ({
| from: (request) => {
| return {
| method: "GET",
Expand All @@ -317,13 +317,13 @@ class CompileMinimalEndpointTest {
| body: serialization.serialize(response.body),
| })
| })
| export const api: Wirespec.Api<Request, Response, Handler> = {
| export const api = {
| name: "getTodos",
| method: "GET",
| path: "todos",
| server,
| client
| }
| } as const
|}
|
|export type TodoDto = {
Expand Down

0 comments on commit 470e464

Please sign in to comment.