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 8 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build/
coverage/
yarn-error.log
.DS_Store
tmp/

# Yarn
.pnp.*
Expand Down
112 changes: 112 additions & 0 deletions HTTP.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Http

If we have to following `.proto` file:

```protobuf
syntax = "proto3";
import "google/api/annotations.proto";
service Messaging {
rpc GetMessage(GetMessageRequest) returns (GetMessageResponse) {
option (google.api.http) = {
get:"/v1/messages/{message_id}"
};
}
rpc CreateMessage(CreateMessageRequest) returns (CreateMessageResponse) {
option (google.api.http) = {
post:"/v1/messages/{message_id}"
body: "message"
};
}
rpc UpdateMessage(CreateMessageRequest) returns (CreateMessageResponse) {
option (google.api.http) = {
patch:"/v1/messages/{message_id}"
body: "*"
};
}
}
message GetMessageRequest {
string message_id = 1;
}
message GetMessageResponse {
string message = 1;
}
message CreateMessageRequest {
string message_id = 1;
string message = 2; // mapped to the body
}
message CreateMessageResponse {}
```

The output will be

```typescript
// ...
export interface GetMessageRequest {
messageId: string;
}

export interface GetMessageResponse {
message: string;
}

export interface CreateMessageRequest {
messageId: string;
/** mapped to the body */
message: string;
}

export interface CreateMessageResponse {}

export const Messaging = {
getMessage: {
path: "/v1/messages/{message_id}",
method: "get",
requestType: undefined as GetMessageRequest,
responseType: undefined as GetMessageResponse,
},

createMessage: {
path: "/v1/messages/{message_id}",
method: "post",
body: "message",
requestType: undefined as CreateMessageRequest,
responseType: undefined as CreateMessageResponse,
},

updateMessage: {
path: "/v1/messages/{message_id}",
method: "patch",
body: "*",
requestType: undefined as CreateMessageRequest,
responseType: undefined as CreateMessageResponse,
},
};
```

## Client implementation example

```typescript
// This is just an example, do not use it directly.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jas-chen , I think this is a great so far--I'm wondering if we could further and remove the need for this createApi method...

I.e. if a user has ~10 services with ~10 methods each, they''ll have to call createApi against each of them--granted, they could write a method to do that in a loop...

Like our current non-google impl looks like:

export interface MyService {
  MyMethod(request: RequestType): Promise<ResponseType>;
}

export const MyServiceServiceName = "MyService";
export class MyServiceClientImpl implements MyService {
  private readonly rpc: Rpc;
  private readonly service: string;
  constructor(rpc: Rpc, opts?: { service?: string }) {
    this.service = opts?.service || MyServiceServiceName;
    this.rpc = rpc;
    this.MyMethod = this.MyMethod.bind(this);
  }
  MyMethod(request: RequestType): Promise<ResponseType> {
    const data = RequestType.encode(request).finish();
    const promise = this.rpc.request(this.service, "MyMethod", data);
    return promise.then((data) => ResponseType.decode(_m0.Reader.create(data)));
  }
}

And it seems like it'd be nice to provide a similar sort of API to users...

I suppose with the export const Messaging, the users could provide their own mapped types & helper methods to achieve this same output, i.e. something like:

// creates the `MessagingService.MyMethod` / `MessagingService.updateMessage` interface
type MessagingService = ClientService<typeof Messaging>;

const client: MessagingService = createClient(Messaging);
await client.getMessage(...);

I'm thinking it'd be great for ts-proto to eventually provide those ClientService and createClient implementations (i.e. that could automatically parse & replace the {message_id} based on the rules in their docs).

Granted, sometimes I've just codegen-d out the export interface MyService { ... } and export class MyServiceImpl, which was a very old-school approach code generation, but it's definitely doable to use TS mapped types & runtime libraries to achieve the same effect.

Wdyt about the goal of shipping a ClientService & createClient method that took in the Messaging config? Maybe it could be like a ts-proto-google-http runtime library, which didn't bloat the codegen output. Would you be interesting in contributing that as well as this config output?

Thanks!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the client implementation example code, the createApi takes a service definition now.

function createApi<T extends { path: string; method: "get"; request?: unknown; response?: unknown }>(config: T) {
return function api(payload: T["requestType"]): Promise<T["responseType"]> {
const path = config.path.replace("{message_id}", payload.messageId);
const method = config.method;
const body = method === "get" ? undefined : JSON.stringify({ message: payload.message });

return fetch(path, { method, body });
};
}

const getMessage = createApi(Messaging.getMessage);

getMessage({
messageId: "123",
}).then((res) => {
// ...
});
```
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.PHONY: build-vendor
build-vendor:
@rm -rf tmp
@mkdir -p tmp/googleapis
@cd tmp/googleapis && \
git init && \
git remote add origin git@github.com:googleapis/googleapis.git && \
git fetch origin 47947b2fb9bdde9b02a7dd173a5077a1cc2beb25 && \
git checkout FETCH_HEAD && \
cd ../..
@rm -rf vendor
@mkdir vendor
@protoc \
--js_out=import_style=commonjs,binary:vendor \
-I tmp/googleapis \
tmp/googleapis/google/api/http.proto tmp/googleapis/google/api/annotations.proto
@rm -rf tmp
14 changes: 12 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 @@ -428,6 +426,10 @@ Generated code will be placed in the Gradle build directory.

Note that `addGrpcMetadata`, `addNestjsRestParameter` and `returnObservable` will still be false.

- With `--ts_proto_opt=http=true`, the defaults will change to generate http friendly types & service metadata that can be used to create your own http client. See the [http readme](HTTP.markdown) for more information and implementation examples.
jas-chen marked this conversation as resolved.
Show resolved Hide resolved

Specifically `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` will all be false, `lowerCaseServiceMethods` will be true and `outputServices` will be ignored.

- With `--ts_proto_opt=useDate=false`, fields of type `google.protobuf.Timestamp` will not be mapped to type `Date` in the generated types. See [Timestamp](#timestamp) for more details.

- With `--ts_proto_opt=useMongoObjectId=true`, fields of a type called ObjectId where the message is constructed to have on field called value that is a string will be mapped to type `mongodb.ObjectId` in the generated types. This will require your project to install the mongodb npm package. See [ObjectId](#objectid) for more details.
Expand Down Expand Up @@ -702,6 +704,14 @@ The commands below assume you have **Docker** installed. If you are using OS X,
> - `proto2pbjs` — Generates a reference implementation using `pbjs` for testing compatibility.
- Run `yarn test`

**Vendor**

To update [vendor](./vendor/) code

1. Install [Code Generator Plugins](https://github.com/grpc/grpc-web?tab=readme-ov-file#code-generator-plugins)
2. Update the commit id in [Makefile](./Makefile#L8).
3. Run `make build-vendor`.

**Workflow**

- Add/update an integration test for your use case
Expand Down
31 changes: 31 additions & 0 deletions integration/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/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