Skip to content

Commit

Permalink
Convenience functions to read service and message options #36
Browse files Browse the repository at this point in the history
  • Loading branch information
timostamm committed May 13, 2021
1 parent 213751d commit c559c0d
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 85 deletions.
138 changes: 69 additions & 69 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ protobuf-ts manual
- [google.type.DateTime and google.type.Date](#googletypedatetime-and-googletypedate)
- [Reflection](#reflection)
- [Field information](#field-information)
- [Custom options](#custom-options)
- [Excluding custom options](#excluding-custom-options)
- [Custom options](#custom-options)
- [Creating your own custom options](#creating-your-own-custom-options)
- [Excluding options from generated code](#excluding-options-from-generated-code)
- [Binary format](#binary-format)
- [Conformance](#conformance)
- [Unknown field handling](#unknown-field-handling)
Expand Down Expand Up @@ -915,20 +916,21 @@ operations.
> `runtime-rpc/src/reflection-info.ts`.

#### Custom options
## Custom options

`protobuf-ts` supports custom message, field, service and method options
and will add them to the reflection information.
`protobuf-ts` supports custom options for messages, fields, services
and methods and will add them to the reflection information.

For example, consider the following service definition in .proto:
For example, consider the following service definition in
[service-annotated.proto](./packages/test-fixtures/service-annotated.proto):

```proto
// import the proto that extends google.protobuf.MethodOptions
import "google/api/annotations.proto";
service AnnotatedService {
rpc Get (Request) returns (Reply) {
// now we can use the new options on the method
// add an option on the method
option (google.api.http) = {
get: "/v1/{name=messages/*}"
additional_bindings {
Expand All @@ -942,35 +944,68 @@ service AnnotatedService {
}
```

In TypeScript generated code, those options look very similar:
In TypeScript, the service options are available in the "options"
property as JSON:

```typescript
/**
* @generated from protobuf service spec.AnnotatedService
*/
export class AnnotatedServiceClient implements IAnnotatedServiceClient {
readonly typeName = "spec.AnnotatedService";
readonly methods: MethodInfo[] = [{
service: this,
name: "Get",
localName: "get",
I: AnnoGetRequest,
O: AnnoGetResponse,
options: {
// here are the options, in JSON format
"google.api.http": {
additionalBindings: [{
get: "xxx"
}, {
get: "yyy"
}],
get: "/v1/{name=messages/*}"
}
}
}];
// ...
import {AnnotatedService} from "./service-annotated";
console.log(AnnotatedService.options);
```

```json
{
"google.api.http": {
additionalBindings: [{
get: "xxx"
}, {
get: "yyy"
}],
get: "/v1/{name=messages/*}"
}
}
```

Because the option "google.api.http" is actually a message
(see [annotations.proto](./packages/test-fixtures/google/api/annotations.proto)),
you can parse the message with this convenience method:


```typescript
import {AnnotatedService} from "./service-annotated";
import {HttpRule} from "./google/api/annotations";
import {readMethodOption} from "@protobuf-ts/runtime-rpc";

let rule = readMethodOption(AnnotatedService, "get", "google.api.http", HttpRule);
if (rule) {
let selector: string = rule.selector;
let bindings: HttpRule[] = rule.additionalBindings;
}
```

If you omit the last parameter to the function, you get the JSON value.
This is a convenient way to get scalar option values.

```typescript
import {AnnotatedService} from "./service-annotated";
import {readMethodOption} from "@protobuf-ts/runtime-rpc";
import {JsonValue} from "@protobuf-ts/runtime";

let rule: JsonValue | undefined = readMethodOption(AnnotatedService, "get", "google.api.http");
```



| Options for | stored in | access with |
|-------------|---------------------------------------|-----------------------------------------------------|
| Messages | `AnnotatedMessage.options` | `readMessageOption()` from @protobuf-ts/runtime |
| Fields | `AnnotatedMessage.field[0].options` | `readFieldOption()` from @protobuf-ts/runtime |
| Services | `AnnotatedService.options` | `readServiceOption()` from @protobuf-ts/runtime-rpc |
| Methods | `AnnotatedService.methods[0].options` | `readMethodOption()` from @protobuf-ts/runtime-rpc |



#### Creating your own custom options

It is very easy to create custom options. This is the source code for the
"google.api.http" option:

Expand All @@ -990,45 +1025,10 @@ As you can see, the option is a standard protobuf field. It can be a message
field like in the example above, or it can be a scalar, enum or repeated field.

In .proto, you set options in [text format](https://stackoverflow.com/questions/18873924/what-does-the-protobuf-text-format-look-like/18877167),
and `protobuf-ts` provides them in the canonical JSON format. This means that
you can parse message option fields using the appropriate message type:
```typescript
let client: AnnotatedServiceClient = ...

let opt = client.methods[0].options;

if (opt && "google.api.http" in opt) {
let rule = HttpRule.fromJson(opt["google.api.http"]);
if (rule) {
// now we have successfully read a
// google.api.HttpRule from the option
let selector: string = rule.selector;
let bindings: HttpRule[] = rule.additionalBindings;
}
}
```
To save you some typing, there is a convenience method available:
```typescript
import {readMethodOptions} from "@protobuf-ts/runtime-rpc";

let rule = readMethodOptions(client, "get", "google.api.http", HttpRule);
if (rule) {
let selector: string = rule.selector;
let bindings: HttpRule[] = rule.additionalBindings;
}
```
Field options work the same way. In .proto, you create an extension for
`google.protobuf.FieldOptions`, and in TypeScript, you can read
`IMessageType.fields[].options`. If it is a message field, use
`readFieldOptions()` from `@protobuf-ts/runtime`.
and `protobuf-ts` provides them in the canonical JSON format.


#### Excluding custom options
#### Excluding options from generated code

If you need custom options for some protobuf implementation, but do not
want to have them included in the TypeScript generated code, use the file
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-rpc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


export {ServiceType} from './service-type';
export {MethodInfo, PartialMethodInfo, ServiceInfo, readMethodOptions} from './reflection-info';
export {MethodInfo, PartialMethodInfo, ServiceInfo, readMethodOptions, readMethodOption, readServiceOption} from './reflection-info';
export {RpcError} from './rpc-error';
export {RpcMetadata} from './rpc-metadata';
export {RpcOptions, mergeRpcOptions} from './rpc-options';
Expand Down
65 changes: 60 additions & 5 deletions packages/runtime-rpc/src/reflection-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export type PartialMethodInfo<I extends object = any, O extends object = any> =
type PartialPartial<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>;



/**
* Turns PartialMethodInfo into MethodInfo.
*/
Expand All @@ -138,10 +137,66 @@ export function normalizeMethodInfo<I extends object = any, O extends object = a

/**
* Read custom method options from a generated service client.
*
* @deprecated use readMethodOption()
*/
export function readMethodOptions<T extends object>(service: ServiceInfo, methodName: string | number, extensionName: string, extensionType: IMessageType<T>): T | undefined {
let info = service.methods.find((m, i) => m.localName === methodName || i === methodName);
return info && info.options && info.options[extensionName]
? extensionType.fromJson(info.options[extensionName])
: undefined;
const options = service.methods.find((m, i) => m.localName === methodName || i === methodName)?.options;
return options && options[extensionName] ? extensionType.fromJson(options[extensionName]) : undefined;
}

/**
* Read a custom method option.
*
* ```proto
* service MyService {
* rpc Get (Req) returns (Res) {
* option (acme.rpc_opt) = true;
* };
* }
* ```
*
* ```typescript
* let val = readMethodOption(MyService, 'get', 'acme.rpc_opt')
* ```
*/
export function readMethodOption<T extends object>(service: ServiceInfo, methodName: string | number, extensionName: string): JsonValue | undefined;
export function readMethodOption<T extends object>(service: ServiceInfo, methodName: string | number, extensionName: string, extensionType: IMessageType<T>): T | undefined;
export function readMethodOption<T extends object>(service: ServiceInfo, methodName: string | number, extensionName: string, extensionType?: IMessageType<T>): T | JsonValue | undefined {
const options = service.methods.find((m, i) => m.localName === methodName || i === methodName)?.options;
if (!options) {
return undefined;
}
const optionVal = options[extensionName];
if (optionVal === undefined) {
return optionVal;
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}

/**
* Read a custom service option.
*
* ```proto
* service MyService {
* option (acme.service_opt) = true;
* }
* ```
*
* ```typescript
* let val = readServiceOption(MyService, 'acme.service_opt')
* ```
*/
export function readServiceOption<T extends object>(service: ServiceInfo, extensionName: string): JsonValue | undefined;
export function readServiceOption<T extends object>(service: ServiceInfo, extensionName: string, extensionType: IMessageType<T>): T | undefined;
export function readServiceOption<T extends object>(service: ServiceInfo, extensionName: string, extensionType?: IMessageType<T>): T | JsonValue | undefined {
const options = service.options;
if (!options) {
return undefined;
}
const optionVal = options[extensionName];
if (optionVal === undefined) {
return optionVal;
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}
4 changes: 3 additions & 1 deletion packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export {
FieldInfo,
PartialFieldInfo,
normalizeFieldInfo,
readFieldOptions
readFieldOptions,
readFieldOption,
readMessageOption
} from './reflection-info';

// Types for messsage objects type at runtime, when concrete type is unknown.
Expand Down
62 changes: 57 additions & 5 deletions packages/runtime/src/reflection-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,62 @@ export function normalizeFieldInfo(field: PartialFieldInfo): FieldInfo {

/**
* Read custom field options from a generated message type.
*
* @deprecated use readFieldOption()
*/
export function readFieldOptions<T extends object>(messageType: MessageInfo, fieldName: string | number, extensionName: string, extensionType: IMessageType<T>): T | undefined {
const options = messageType.fields.find((m, i) => m.localName == fieldName || i == fieldName)?.options;
return options && options[extensionName] ? extensionType.fromJson(options[extensionName]) : undefined;
}


/**
* Read a custom field option.
*
* ```proto
* message MyMessage {
* int32 my_field = 1 [(acme.field_opt) = true];
* }
* ```
*
* ```typescript
* let val = readFieldOption(MyMessage, 'myField', 'acme.field_opt')
* ```
*/
export function readFieldOption<T extends object>(messageType: MessageInfo, fieldName: string | number, extensionName: string): JsonValue | undefined;
export function readFieldOption<T extends object>(messageType: MessageInfo, fieldName: string | number, extensionName: string, extensionType: IMessageType<T>): T | undefined;
export function readFieldOption<T extends object>(messageType: MessageInfo, fieldName: string | number, extensionName: string, extensionType?: IMessageType<T>): T | JsonValue | undefined {
const options = messageType.fields.find((m, i) => m.localName == fieldName || i == fieldName)?.options;
if (!options) {
return undefined;
}
const optionVal = options[extensionName];
if (optionVal === undefined) {
return optionVal;
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}

/**
* Read a custom message option.
*
* ```proto
* message MyMessage {
* option acme.message_opt = true;
* }
* ```
*
* ```typescript
* let val = readMessageOption(MyMessage, 'acme.message_opt')
* ```
*/
export function readFieldOptions<T extends object>(messageType: MessageInfo | IMessageType<any>, fieldName: string | number, extensionName: string, extensionType: IMessageType<T>): T | undefined {
let info = messageType.fields.find((m, i) => m.localName == fieldName || i == fieldName);
return info && info.options && info.options[extensionName]
? extensionType.fromJson(info.options[extensionName])
: undefined;
export function readMessageOption<T extends object>(messageType: MessageInfo, extensionName: string): JsonValue | undefined;
export function readMessageOption<T extends object>(messageType: MessageInfo, extensionName: string, extensionType: IMessageType<T>): T | undefined
export function readMessageOption<T extends object>(messageType: MessageInfo, extensionName: string, extensionType?: IMessageType<T>): T | JsonValue | undefined {
const options = messageType.options;
const optionVal = options[extensionName];
if (optionVal === undefined) {
return optionVal;
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}
Loading

0 comments on commit c559c0d

Please sign in to comment.