diff --git a/integration/fieldmask/fieldmask-test.ts b/integration/fieldmask/fieldmask-test.ts new file mode 100644 index 000000000..2be785af4 --- /dev/null +++ b/integration/fieldmask/fieldmask-test.ts @@ -0,0 +1,27 @@ +import { FieldMaskMessage } from './fieldmask'; + +let data = { + fieldMask: 'a,b,c.d', +}; + +describe('fieldmask', () => { + it('can decode JSON', () => { + const f = FieldMaskMessage.fromJSON(data); + expect(f).toMatchInlineSnapshot(` + Object { + "fieldMask": Object { + "paths": Array [ + "a", + "b", + "c.d", + ], + }, + } + `); + }); + + it('can encode JSON', () => { + const f = FieldMaskMessage.toJSON({ fieldMask: { paths: ['a', 'b', 'c.d'] } }); + expect(f).toEqual(data); + }); +}); diff --git a/integration/fieldmask/fieldmask.bin b/integration/fieldmask/fieldmask.bin new file mode 100644 index 000000000..7a228dc33 Binary files /dev/null and b/integration/fieldmask/fieldmask.bin differ diff --git a/integration/fieldmask/fieldmask.proto b/integration/fieldmask/fieldmask.proto new file mode 100644 index 000000000..86556d3c5 --- /dev/null +++ b/integration/fieldmask/fieldmask.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +import "google/protobuf/field_mask.proto"; + +message FieldMaskMessage { + google.protobuf.FieldMask field_mask = 1; +} diff --git a/integration/fieldmask/fieldmask.ts b/integration/fieldmask/fieldmask.ts new file mode 100644 index 000000000..cea74768c --- /dev/null +++ b/integration/fieldmask/fieldmask.ts @@ -0,0 +1,88 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; +import { FieldMask } from './google/protobuf/field_mask'; + +export const protobufPackage = ''; + +export interface FieldMaskMessage { + fieldMask: FieldMask | undefined; +} + +function createBaseFieldMaskMessage(): FieldMaskMessage { + return { fieldMask: undefined }; +} + +export const FieldMaskMessage = { + encode(message: FieldMaskMessage, writer: Writer = Writer.create()): Writer { + if (message.fieldMask !== undefined) { + FieldMask.encode(message.fieldMask, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): FieldMaskMessage { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFieldMaskMessage(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.fieldMask = FieldMask.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): FieldMaskMessage { + return { + fieldMask: isSet(object.fieldMask) ? { paths: object.fieldMask.split(',') } : undefined, + }; + }, + + toJSON(message: FieldMaskMessage): unknown { + const obj: any = {}; + message.fieldMask !== undefined && (obj.fieldMask = message.fieldMask.paths.join()); + return obj; + }, + + fromPartial, I>>(object: I): FieldMaskMessage { + const message = createBaseFieldMaskMessage(); + message.fieldMask = + object.fieldMask !== undefined && object.fieldMask !== null ? FieldMask.fromPartial(object.fieldMask) : undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/fieldmask/google/protobuf/field_mask.ts b/integration/fieldmask/google/protobuf/field_mask.ts new file mode 100644 index 000000000..e656bf95f --- /dev/null +++ b/integration/fieldmask/google/protobuf/field_mask.ts @@ -0,0 +1,288 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'google.protobuf'; + +/** + * `FieldMask` represents a set of symbolic field paths, for example: + * + * paths: "f.a" + * paths: "f.b.d" + * + * Here `f` represents a field in some root message, `a` and `b` + * fields in the message found in `f`, and `d` a field found in the + * message in `f.b`. + * + * Field masks are used to specify a subset of fields that should be + * returned by a get operation or modified by an update operation. + * Field masks also have a custom JSON encoding (see below). + * + * # Field Masks in Projections + * + * When used in the context of a projection, a response message or + * sub-message is filtered by the API to only contain those fields as + * specified in the mask. For example, if the mask in the previous + * example is applied to a response message as follows: + * + * f { + * a : 22 + * b { + * d : 1 + * x : 2 + * } + * y : 13 + * } + * z: 8 + * + * The result will not contain specific values for fields x,y and z + * (their value will be set to the default, and omitted in proto text + * output): + * + * + * f { + * a : 22 + * b { + * d : 1 + * } + * } + * + * A repeated field is not allowed except at the last position of a + * paths string. + * + * If a FieldMask object is not present in a get operation, the + * operation applies to all fields (as if a FieldMask of all fields + * had been specified). + * + * Note that a field mask does not necessarily apply to the + * top-level response message. In case of a REST get operation, the + * field mask applies directly to the response, but in case of a REST + * list operation, the mask instead applies to each individual message + * in the returned resource list. In case of a REST custom method, + * other definitions may be used. Where the mask applies will be + * clearly documented together with its declaration in the API. In + * any case, the effect on the returned resource/resources is required + * behavior for APIs. + * + * # Field Masks in Update Operations + * + * A field mask in update operations specifies which fields of the + * targeted resource are going to be updated. The API is required + * to only change the values of the fields as specified in the mask + * and leave the others untouched. If a resource is passed in to + * describe the updated values, the API ignores the values of all + * fields not covered by the mask. + * + * If a repeated field is specified for an update operation, new values will + * be appended to the existing repeated field in the target resource. Note that + * a repeated field is only allowed in the last position of a `paths` string. + * + * If a sub-message is specified in the last position of the field mask for an + * update operation, then new value will be merged into the existing sub-message + * in the target resource. + * + * For example, given the target message: + * + * f { + * b { + * d: 1 + * x: 2 + * } + * c: [1] + * } + * + * And an update message: + * + * f { + * b { + * d: 10 + * } + * c: [2] + * } + * + * then if the field mask is: + * + * paths: ["f.b", "f.c"] + * + * then the result will be: + * + * f { + * b { + * d: 10 + * x: 2 + * } + * c: [1, 2] + * } + * + * An implementation may provide options to override this default behavior for + * repeated and message fields. + * + * In order to reset a field's value to the default, the field must + * be in the mask and set to the default value in the provided resource. + * Hence, in order to reset all fields of a resource, provide a default + * instance of the resource and set all fields in the mask, or do + * not provide a mask as described below. + * + * If a field mask is not present on update, the operation applies to + * all fields (as if a field mask of all fields has been specified). + * Note that in the presence of schema evolution, this may mean that + * fields the client does not know and has therefore not filled into + * the request will be reset to their default. If this is unwanted + * behavior, a specific service may require a client to always specify + * a field mask, producing an error if not. + * + * As with get operations, the location of the resource which + * describes the updated values in the request message depends on the + * operation kind. In any case, the effect of the field mask is + * required to be honored by the API. + * + * ## Considerations for HTTP REST + * + * The HTTP kind of an update operation which uses a field mask must + * be set to PATCH instead of PUT in order to satisfy HTTP semantics + * (PUT must only be used for full updates). + * + * # JSON Encoding of Field Masks + * + * In JSON, a field mask is encoded as a single string where paths are + * separated by a comma. Fields name in each path are converted + * to/from lower-camel naming conventions. + * + * As an example, consider the following message declarations: + * + * message Profile { + * User user = 1; + * Photo photo = 2; + * } + * message User { + * string display_name = 1; + * string address = 2; + * } + * + * In proto a field mask for `Profile` may look as such: + * + * mask { + * paths: "user.display_name" + * paths: "photo" + * } + * + * In JSON, the same mask is represented as below: + * + * { + * mask: "user.displayName,photo" + * } + * + * # Field Masks and Oneof Fields + * + * Field masks treat fields in oneofs just as regular fields. Consider the + * following message: + * + * message SampleMessage { + * oneof test_oneof { + * string name = 4; + * SubMessage sub_message = 9; + * } + * } + * + * The field mask can be: + * + * mask { + * paths: "name" + * } + * + * Or: + * + * mask { + * paths: "sub_message" + * } + * + * Note that oneof type names ("test_oneof" in this case) cannot be used in + * paths. + * + * ## Field Mask Verification + * + * The implementation of any API method which has a FieldMask type field in the + * request should verify the included field paths, and return an + * `INVALID_ARGUMENT` error if any path is unmappable. + */ +export interface FieldMask { + /** The set of field mask paths. */ + paths: string[]; +} + +function createBaseFieldMask(): FieldMask { + return { paths: [] }; +} + +export const FieldMask = { + encode(message: FieldMask, writer: Writer = Writer.create()): Writer { + for (const v of message.paths) { + writer.uint32(10).string(v!); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): FieldMask { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFieldMask(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.paths.push(reader.string()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): FieldMask { + return { + paths: Array.isArray(object?.paths) ? object.paths.map((e: any) => String(e)) : [], + }; + }, + + toJSON(message: FieldMask): unknown { + const obj: any = {}; + if (message.paths) { + obj.paths = message.paths.map((e) => e); + } else { + obj.paths = []; + } + return obj; + }, + + fromPartial, I>>(object: I): FieldMask { + const message = createBaseFieldMask(); + message.paths = object.paths?.map((e) => e) || []; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/src/main.ts b/src/main.ts index 120c36925..687e12be3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import { isBytes, isBytesValueType, isEnum, + isFieldMaskType, isListValueType, isListValueTypeName, isLong, @@ -1146,6 +1147,8 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto return code`${utils.fromJsonTimestamp}(${from})`; } else if (isAnyValueType(field) || isStructType(field)) { return code`${from}`; + } else if (isFieldMaskType(field)) { + return code`{paths: ${from}.split(",")}`; } else if (isListValueType(field)) { return code`[...${from}]`; } else if (isValueType(ctx, field)) { @@ -1335,6 +1338,8 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP } } else if (isAnyValueType(field)) { return code`${from}`; + } else if (isFieldMaskType(field)) { + return code`${from}.paths.join()`; } else if (isMessage(field) && !isValueType(ctx, field) && !isMapType(ctx, messageDesc, field)) { const type = basicTypeName(ctx, field, { keepValueType: true }); return code`${from} ? ${type}.toJSON(${from}) : ${defaultValue(ctx, field)}`; @@ -1495,7 +1500,7 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri && object.${oneofName}?.${fieldName} !== null ) { message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${v} }; - } + } `); } else if (readSnippet(`x`).toCodeString() == 'x') { // An optimized case of the else below that works when `readSnippet` returns the plain input @@ -1535,7 +1540,7 @@ function generateWrap(ctx: Context, fullProtoTypeName: string): Code[] { if (ctx.options.oneof === OneofOption.UNIONS) { chunks.push(code`wrap(value: any): Value { const result = createBaseValue(); - + if (value === null) { result.kind = {$case: 'nullValue', nullValue: NullValue.NULL_VALUE}; } else if (typeof value === 'boolean') { diff --git a/src/types.ts b/src/types.ts index 1bacd5f9a..29419d7ee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -429,6 +429,14 @@ export function isBytesValueType(field: FieldDescriptorProto): boolean { return field.typeName === '.google.protobuf.BytesValue'; } +export function isFieldMaskType(field: FieldDescriptorProto): boolean { + return isFieldMaskTypeName(field.typeName); +} + +export function isFieldMaskTypeName(typeName: string): boolean { + return typeName === 'google.protobuf.FieldMask' || typeName === '.google.protobuf.FieldMask'; +} + export function isListValueType(field: FieldDescriptorProto): boolean { return isListValueTypeName(field.typeName); }