From 25fc7a004f3d40477a45ebd0209dd31735cbc0c8 Mon Sep 17 00:00:00 2001 From: Timo Saikkonen Date: Wed, 25 Oct 2023 14:42:00 +0300 Subject: [PATCH] fix: return types and optional chaining in field masks when `useOptionals=all` --- .../fieldmask-optional.bin | Bin 0 -> 8097 bytes .../fieldmask-optional.proto | 7 + .../fieldmask-optional.ts | 82 +++++ .../fieldmask-optional-all/fieldmask-test.ts | 59 ++++ .../google/protobuf/field_mask.ts | 292 ++++++++++++++++++ .../fieldmask-optional-all/parameters.txt | 1 + src/generate-struct-wrappers.ts | 17 +- src/main.ts | 15 +- 8 files changed, 463 insertions(+), 10 deletions(-) create mode 100644 integration/fieldmask-optional-all/fieldmask-optional.bin create mode 100644 integration/fieldmask-optional-all/fieldmask-optional.proto create mode 100644 integration/fieldmask-optional-all/fieldmask-optional.ts create mode 100644 integration/fieldmask-optional-all/fieldmask-test.ts create mode 100644 integration/fieldmask-optional-all/google/protobuf/field_mask.ts create mode 100644 integration/fieldmask-optional-all/parameters.txt diff --git a/integration/fieldmask-optional-all/fieldmask-optional.bin b/integration/fieldmask-optional-all/fieldmask-optional.bin new file mode 100644 index 0000000000000000000000000000000000000000..aabfe9ce5b5a1d7b2eb8fd50d59088a6e4417a0b GIT binary patch literal 8097 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 literal 0 HcmV?d00001 diff --git a/integration/fieldmask-optional-all/fieldmask-optional.proto b/integration/fieldmask-optional-all/fieldmask-optional.proto new file mode 100644 index 000000000..4dca52e5e --- /dev/null +++ b/integration/fieldmask-optional-all/fieldmask-optional.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +import "google/protobuf/field_mask.proto"; + +message Example { + google.protobuf.FieldMask mask = 1; +} diff --git a/integration/fieldmask-optional-all/fieldmask-optional.ts b/integration/fieldmask-optional-all/fieldmask-optional.ts new file mode 100644 index 000000000..6062674c2 --- /dev/null +++ b/integration/fieldmask-optional-all/fieldmask-optional.ts @@ -0,0 +1,82 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; +import { FieldMask } from "./google/protobuf/field_mask"; + +export const protobufPackage = ""; + +export interface Example { + mask?: string[] | undefined; +} + +function createBaseExample(): Example { + return { mask: undefined }; +} + +export const Example = { + encode(message: Example, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.mask !== undefined) { + FieldMask.encode(FieldMask.wrap(message.mask), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Example { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExample(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.mask = FieldMask.unwrap(FieldMask.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Example { + return { mask: isSet(object.mask) ? FieldMask.unwrap(FieldMask.fromJSON(object.mask)) : undefined }; + }, + + toJSON(message: Example): unknown { + const obj: any = {}; + if (message.mask !== undefined) { + obj.mask = FieldMask.toJSON(FieldMask.wrap(message.mask)); + } + return obj; + }, + + create, I>>(base?: I): Example { + return Example.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Example { + const message = createBaseExample(); + message.mask = object.mask ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.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 } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/fieldmask-optional-all/fieldmask-test.ts b/integration/fieldmask-optional-all/fieldmask-test.ts new file mode 100644 index 000000000..4acd80b5c --- /dev/null +++ b/integration/fieldmask-optional-all/fieldmask-test.ts @@ -0,0 +1,59 @@ +import { Example } from "./fieldmask-optional"; + +let data = { + mask: "a,b,c.d", +}; + +describe("fieldmask-optional-all", () => { + it("can decode canonical JSON", () => { + const f = Example.fromJSON(data); + expect(f).toMatchInlineSnapshot(` + { + "mask": [ + "a", + "b", + "c.d", + ], + } + `); + }); + + it("can decode non-canonical JSON", () => { + const f = Example.fromJSON({ + mask: { + paths: ["a", "b", "c.d"], + }, + }); + expect(f).toMatchInlineSnapshot(` + { + "mask": [ + "a", + "b", + "c.d", + ], + } + `); + }); + + it("can encode JSON", () => { + const f = Example.toJSON({ mask: ["a", "b", "c.d"] }); + expect(f).toEqual(data); + }); + + it("can encode JSON with undefined input", () => { + const f = Example.toJSON({ mask: undefined }); + expect(f).toEqual({ mask: undefined }); + }); + + it("skips empty paths", () => { + const f = Example.fromJSON({ mask: "a,,c.d" }); + expect(f).toMatchInlineSnapshot(` + { + "mask": [ + "a", + "c.d", + ], + } + `); + }); +}); diff --git a/integration/fieldmask-optional-all/google/protobuf/field_mask.ts b/integration/fieldmask-optional-all/google/protobuf/field_mask.ts new file mode 100644 index 000000000..0bc022ccc --- /dev/null +++ b/integration/fieldmask-optional-all/google/protobuf/field_mask.ts @@ -0,0 +1,292 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; + +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[] | undefined; +} + +function createBaseFieldMask(): FieldMask { + return { paths: [] }; +} + +export const FieldMask = { + encode(message: FieldMask, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.paths !== undefined && message.paths.length !== 0) { + for (const v of message.paths) { + writer.uint32(10).string(v!); + } + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): FieldMask { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(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: + if (tag !== 10) { + break; + } + + message.paths!.push(reader.string()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): FieldMask { + return { + paths: typeof (object) === "string" + ? object.split(",").filter(globalThis.Boolean) + : globalThis.Array.isArray(object?.paths) + ? object.paths.map(globalThis.String) + : [], + }; + }, + + toJSON(message: FieldMask): string | undefined { + return message.paths?.join(","); + }, + + create, I>>(base?: I): FieldMask { + return FieldMask.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): FieldMask { + const message = createBaseFieldMask(); + message.paths = object.paths?.map((e) => e) || []; + return message; + }, + + wrap(paths: string[]): FieldMask { + const result = createBaseFieldMask(); + result.paths = paths; + return result; + }, + + unwrap(message: FieldMask): string[] | undefined { + return message?.paths; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.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 } & { [K in Exclude>]: never }; diff --git a/integration/fieldmask-optional-all/parameters.txt b/integration/fieldmask-optional-all/parameters.txt new file mode 100644 index 000000000..c4b34e132 --- /dev/null +++ b/integration/fieldmask-optional-all/parameters.txt @@ -0,0 +1 @@ +useOptionals=all diff --git a/src/generate-struct-wrappers.ts b/src/generate-struct-wrappers.ts index 3ed8caeb2..7a55ae34d 100644 --- a/src/generate-struct-wrappers.ts +++ b/src/generate-struct-wrappers.ts @@ -150,9 +150,7 @@ export function generateUnwrapDeep(ctx: Context, fullProtoTypeName: string, fiel } if (isFieldMaskTypeName(fullProtoTypeName)) { - chunks.push(code`unwrap(message: ${ctx.options.useReadonlyTypes ? "any" : "FieldMask"}): string[] { - return message.paths; - }`); + chunks.push(generateFieldMaskUnwrap(ctx)); } return chunks; @@ -324,14 +322,21 @@ export function generateUnwrapShallow(ctx: Context, fullProtoTypeName: string, f } if (isFieldMaskTypeName(fullProtoTypeName)) { - chunks.push(code`unwrap(message: ${ctx.options.useReadonlyTypes ? "any" : "FieldMask"}): string[] { - return message.paths; - }`); + chunks.push(generateFieldMaskUnwrap(ctx)); } return chunks; } +function generateFieldMaskUnwrap(ctx: Context): Code { + const returnType = ctx.options.useOptionals === "all" ? "string[] | undefined" : "string[]"; + const pathModifier = ctx.options.useOptionals === "all" ? "?" : ""; + + return code`unwrap(message: ${ctx.options.useReadonlyTypes ? "any" : "FieldMask"}): ${returnType} { + return message${pathModifier}.paths; + }`; +} + function maybeReadonly(options: Options): string { return options.useReadonlyTypes ? "readonly " : ""; } diff --git a/src/main.ts b/src/main.ts index dd2887089..00f59eb73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1995,11 +1995,18 @@ function generateFromJson(ctx: Context, fullName: string, fullTypeName: string, return joinCode(chunks, { on: "\n" }); } -function generateCanonicalToJson(fullName: string, fullProtobufTypeName: string): Code | undefined { +function generateCanonicalToJson( + fullName: string, + fullProtobufTypeName: string, + { useOptionals }: Options, +): Code | undefined { if (isFieldMaskTypeName(fullProtobufTypeName)) { + const returnType = useOptionals === "all" ? "string | undefined" : "string"; + const pathModifier = useOptionals === "all" ? "?" : ""; + return code` - toJSON(message: ${fullName}): string { - return message.paths.join(','); + toJSON(message: ${fullName}): ${returnType} { + return message.paths${pathModifier}.join(','); } `; } @@ -2015,7 +2022,7 @@ function generateToJson( const { options, utils, typeMap } = ctx; const chunks: Code[] = []; - const canonicalToJson = generateCanonicalToJson(fullName, fullProtobufTypeName); + const canonicalToJson = generateCanonicalToJson(fullName, fullProtobufTypeName, options); if (canonicalToJson) { chunks.push(canonicalToJson); return joinCode(chunks, { on: "\n" });