Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add google.api.http support #1075

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
82 changes: 82 additions & 0 deletions GOOGLE-API-HTTP.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Generate generic definitions from [google.api.http](https://cloud.google.com/endpoints/docs/grpc/transcoding)

To generate `.ts` files for your `.proto` files that contain `google.api.http`, you can use the `--ts_proto_opt=onlyTypes=true,outputServices=generic-google-api-http-definitions` option.

Please refer to [integration/google-api-http](./integration/google-api-http) for an input/output example.

## Client implementation example

```typescript
import { PathTemplate } from "google-gax";
import { excludeKeys } from "filter-obj";

function createApi<
S extends Record<string, { path: string; method: string; body?: string; requestType: any; responseType: any }>,
>(
fetcher: (input: { path: string; method: string; body?: string }) => Promise<unknown>,
serviceDef: S,
): { [K in keyof S]: (payload: S[K]["requestType"]) => Promise<S[K]["responseType"]> } {
// @ts-expect-error
return Object.fromEntries(
Object.entries(serviceDef).map(([name, endpointDef]) => {
return [
name,
async (payload: typeof endpointDef.requestType): Promise<typeof endpointDef.responseType> => {
const { method, body: bodyKey } = endpointDef;
const pathTemplate = new PathTemplate(endpointDef.path);
const path = pathTemplate.render(payload);
const remainPayload = excludeKeys(payload, Object.keys(pathTemplate.match(path)));

if (bodyKey === "*") {
const body = JSON.stringify(remainPayload);
return fetcher({ path, method, body });
}

let body: string | undefined = undefined;

if (bodyKey) {
body = JSON.stringify({ [bodyKey]: payload[bodyKey] });
delete remainPayload[bodyKey];
}

const qs = new URLSearchParams(remainPayload).toString();
if (qs) {
path += "?" + qs;
}

return fetcher({ path, method, body });
},
];
}),
);
}

async function fetcher(input: { path: string; method: string; body?: string }) {
const url = "http://localhost:8080" + input.path;
const init: RequestInit = {
method: input.method,
headers: {
"Content-Type": "application/json",
},
body: input.body,
};

const res = await fetch(url, init);

if (res.ok) {
return await res.json();
}

throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
}

const api = createApi(fetcher, Messaging);

api
.GetMessage({
messageId: "123",
})
.then((res) => {
console.log(res);
});
```
6 changes: 4 additions & 2 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ If you'd like an out-of-the-box RPC framework built on top of ts-proto, there ar

(Note for potential contributors, if you develop other frameworks/mini-frameworks, or even blog posts/tutorials, on using `ts-proto`, we're happy to link to them.)

We also don't support clients for `google.api.http`-based [Google Cloud](https://cloud.google.com/endpoints/docs/grpc/transcoding) APIs, see [#948](https://github.com/stephenh/ts-proto/issues/948) if you'd like to submit a PR.

# Example Types

The generated types are "just data", i.e.:
Expand Down Expand Up @@ -444,6 +442,10 @@ Generated code will be placed in the Gradle build directory.

- With `--ts_proto_opt=outputServices=generic-definitions`, ts-proto will output generic (framework-agnostic) service definitions. These definitions contain descriptors for each method with links to request and response types, which allows to generate server and client stubs at runtime, and also generate strong types for them at compile time. An example of a library that uses this approach is [nice-grpc](https://github.com/deeplay-io/nice-grpc).

- With `--ts_proto_opt=outputServices=generic-google-api-http-definitions`, ts-proto will output generic (framework-agnostic) service definitions from [google.api.http](https://cloud.google.com/endpoints/docs/grpc/transcoding). These definitions contain descriptors for each method with links to request and response types, which allows to implement a http client based on it. For more information see the [google.api.http readme](GOOGLE-API-HTTP.markdown).

(Requires `onlyTypes=true`.)

- With `--ts_proto_opt=outputServices=nice-grpc`, ts-proto will output server and client stubs for [nice-grpc](https://github.com/deeplay-io/nice-grpc). This should be used together with generic definitions, i.e. you should specify two options: `outputServices=nice-grpc,outputServices=generic-definitions`.

- With `--ts_proto_opt=metadataType=Foo@./some-file`, ts-proto add a generic (framework-agnostic) metadata field to the generic service definition.
Expand Down
49 changes: 49 additions & 0 deletions integration/google-api-http/google-api-http-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @jest-environment node
*/
import { Messaging, GetMessageRequest, GetMessageResponse } from "./simple";

describe("google-api-http-test", () => {
it("compiles", () => {
expect(Messaging).toStrictEqual({
GetMessage: {
path: "/v1/messages/{message_id}",
method: "get",
requestType: undefined,
responseType: undefined,
},
CreateMessage: {
path: "/v1/messages/{message_id}",
method: "post",
body: "message",
requestType: undefined,
responseType: undefined,
},
UpdateMessage: {
path: "/v1/messages/{message_id}",
method: "patch",
body: "*",
requestType: undefined,
responseType: undefined,
},
DeleteMessage: {
path: "/v1/messages/{message_id}",
method: "delete",
body: "*",
requestType: undefined,
responseType: undefined,
},
});

// Test that the request and response types are correctly typed
const copy = { ...Messaging.GetMessage };
const request: GetMessageRequest = {
messageId: "1",
};
const response: GetMessageResponse = {
message: "hello",
};
copy.requestType = request;
copy.responseType = response;
});
});
31 changes: 31 additions & 0 deletions integration/google-api-http/google/api/annotations.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

package google.api;

import "google/api/http.proto";
import "google/protobuf/descriptor.proto";

option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";

extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}
6 changes: 6 additions & 0 deletions integration/google-api-http/google/api/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// source: google/api/annotations.proto

/* eslint-disable */

export const protobufPackage = "google.api";
Loading
Loading