From d82ea4841de822858b0928bd3df294c1064d125e Mon Sep 17 00:00:00 2001 From: Oliver Yasuna Date: Sun, 30 Jun 2024 12:44:29 -0400 Subject: [PATCH] Support strict undefined properties. --- .../use-strict-undefined/parameters.txt | 1 + .../use-strict-undefined.proto | 18 ++ .../use-strict-undefined.ts | 267 ++++++++++++++++++ src/main.ts | 26 +- src/options.ts | 2 + src/types.ts | 2 +- tests/options-test.ts | 1 + 7 files changed, 307 insertions(+), 10 deletions(-) create mode 100644 integration/use-strict-undefined/parameters.txt create mode 100644 integration/use-strict-undefined/use-strict-undefined.proto create mode 100644 integration/use-strict-undefined/use-strict-undefined.ts diff --git a/integration/use-strict-undefined/parameters.txt b/integration/use-strict-undefined/parameters.txt new file mode 100644 index 000000000..21abe1e55 --- /dev/null +++ b/integration/use-strict-undefined/parameters.txt @@ -0,0 +1 @@ +oneof=unions,useStrictUndefined=true diff --git a/integration/use-strict-undefined/use-strict-undefined.proto b/integration/use-strict-undefined/use-strict-undefined.proto new file mode 100644 index 000000000..2866c0162 --- /dev/null +++ b/integration/use-strict-undefined/use-strict-undefined.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +message UseStrictUndefined { + message Inner { + } + + message A { + } + + message B { + } + + Inner inner = 1; + oneof value { + A a = 2; + B b = 3; + } +} diff --git a/integration/use-strict-undefined/use-strict-undefined.ts b/integration/use-strict-undefined/use-strict-undefined.ts new file mode 100644 index 000000000..049f52bab --- /dev/null +++ b/integration/use-strict-undefined/use-strict-undefined.ts @@ -0,0 +1,267 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// source: use-strict-undefined.proto + +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; + +export const protobufPackage = ""; + +export interface UseStrictUndefined { + inner: UseStrictUndefined_Inner; + value: { $case: "a"; a: UseStrictUndefined_A } | { $case: "b"; b: UseStrictUndefined_B }; +} + +export interface UseStrictUndefined_Inner { +} + +export interface UseStrictUndefined_A { +} + +export interface UseStrictUndefined_B { +} + +function createBaseUseStrictUndefined(): UseStrictUndefined { + return { inner: undefined, value: undefined } as unknown as UseStrictUndefined; +} + +export const UseStrictUndefined = { + encode(message: UseStrictUndefined, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.inner !== undefined) { + UseStrictUndefined_Inner.encode(message.inner, writer.uint32(10).fork()).ldelim(); + } + switch (message.value?.$case) { + case "a": + UseStrictUndefined_A.encode(message.value.a, writer.uint32(18).fork()).ldelim(); + break; + case "b": + UseStrictUndefined_B.encode(message.value.b, writer.uint32(26).fork()).ldelim(); + break; + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UseStrictUndefined { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUseStrictUndefined(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.inner = UseStrictUndefined_Inner.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.value = { $case: "a", a: UseStrictUndefined_A.decode(reader, reader.uint32()) }; + continue; + case 3: + if (tag !== 26) { + break; + } + + message.value = { $case: "b", b: UseStrictUndefined_B.decode(reader, reader.uint32()) }; + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): DeepPartial { + return { + inner: isSet(object.inner) ? UseStrictUndefined_Inner.fromJSON(object.inner) : undefined, + value: isSet(object.a) + ? { $case: "a", a: UseStrictUndefined_A.fromJSON(object.a) } + : isSet(object.b) + ? { $case: "b", b: UseStrictUndefined_B.fromJSON(object.b) } + : undefined, + }; + }, + + toJSON(message: UseStrictUndefined): unknown { + const obj: any = {}; + if (message.inner !== undefined) { + obj.inner = UseStrictUndefined_Inner.toJSON(message.inner); + } + if (message.value?.$case === "a") { + obj.a = UseStrictUndefined_A.toJSON(message.value.a); + } + if (message.value?.$case === "b") { + obj.b = UseStrictUndefined_B.toJSON(message.value.b); + } + return obj; + }, + + create, I>>(base?: I): DeepPartial { + return UseStrictUndefined.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeepPartial { + const message = createBaseUseStrictUndefined(); + if (object.inner !== undefined && object.inner !== null) { + message.inner = UseStrictUndefined_Inner.fromPartial(object.inner); + } + if (object.value?.$case === "a" && object.value?.a !== undefined && object.value?.a !== null) { + message.value = { $case: "a", a: UseStrictUndefined_A.fromPartial(object.value.a) }; + } + if (object.value?.$case === "b" && object.value?.b !== undefined && object.value?.b !== null) { + message.value = { $case: "b", b: UseStrictUndefined_B.fromPartial(object.value.b) }; + } + return message; + }, +}; + +function createBaseUseStrictUndefined_Inner(): UseStrictUndefined_Inner { + return {} as unknown as UseStrictUndefined_Inner; +} + +export const UseStrictUndefined_Inner = { + encode(_: UseStrictUndefined_Inner, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UseStrictUndefined_Inner { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUseStrictUndefined_Inner(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): DeepPartial { + return {}; + }, + + toJSON(_: UseStrictUndefined_Inner): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): DeepPartial { + return UseStrictUndefined_Inner.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): DeepPartial { + const message = createBaseUseStrictUndefined_Inner(); + return message; + }, +}; + +function createBaseUseStrictUndefined_A(): UseStrictUndefined_A { + return {} as unknown as UseStrictUndefined_A; +} + +export const UseStrictUndefined_A = { + encode(_: UseStrictUndefined_A, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UseStrictUndefined_A { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUseStrictUndefined_A(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): DeepPartial { + return {}; + }, + + toJSON(_: UseStrictUndefined_A): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): DeepPartial { + return UseStrictUndefined_A.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): DeepPartial { + const message = createBaseUseStrictUndefined_A(); + return message; + }, +}; + +function createBaseUseStrictUndefined_B(): UseStrictUndefined_B { + return {} as unknown as UseStrictUndefined_B; +} + +export const UseStrictUndefined_B = { + encode(_: UseStrictUndefined_B, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UseStrictUndefined_B { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUseStrictUndefined_B(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): DeepPartial { + return {}; + }, + + toJSON(_: UseStrictUndefined_B): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): DeepPartial { + return UseStrictUndefined_B.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): DeepPartial { + const message = createBaseUseStrictUndefined_B(); + 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 { $case: string } ? { [K in keyof Omit]?: DeepPartial } & { $case: T["$case"] } + : 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/src/main.ts b/src/main.ts index 3cbe7ea0e..721497550 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1023,7 +1023,7 @@ function generateOneofProperty( ); const name = maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, options); - return code`${mbReadonly}${name}?: ${unionType} | ${nullOrUndefined(options)},`; + return code`${mbReadonly}${name}${ctx.options.useStrictUndefined ? '' : '?'}: ${unionType}${ctx.options.useStrictUndefined ? '' : ` | ${nullOrUndefined(options)}`},`; /* // Ideally we'd put the comments for each oneof field next to the anonymous @@ -1105,7 +1105,7 @@ function generateBaseInstanceFactory( return code` function createBase${fullName}(): ${fullName} { - return { ${joinCode(fields, { on: "," })} }; + return { ${joinCode(fields, { on: "," })} }${options.useStrictUndefined ? `as unknown as ${fullName}` : ''}; } `; } @@ -1938,7 +1938,7 @@ function generateFromJson(ctx: Context, fullName: string, fullTypeName: string, // create the basic function declaration chunks.push(code` - fromJSON(${messageDesc.field.length > 0 ? "object" : "_"}: any): ${fullName} { + fromJSON(${messageDesc.field.length > 0 ? "object" : "_"}: any): ${ctx.options.useStrictUndefined ? `DeepPartial<${fullName}>` : fullName} { return { `); @@ -2376,13 +2376,13 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri // create the create function definition if (ctx.options.useExactTypes) { chunks.push(code` - create, I>>(base?: I): ${fullName} { + create, I>>(base?: I): ${options.useStrictUndefined ? `DeepPartial<${fullName}>` : fullName} { return ${fullName}.fromPartial(base ?? ({} as any)); }, `); } else { chunks.push(code` - create(base?: ${utils.DeepPartial}<${fullName}>): ${fullName} { + create(base?: ${utils.DeepPartial}<${fullName}>): ${options.useStrictUndefined ? `DeepPartial<${fullName}>` : fullName} { return ${fullName}.fromPartial(base ?? {}); }, `); @@ -2393,11 +2393,11 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri if (ctx.options.useExactTypes) { chunks.push(code` - fromPartial, I>>(${paramName}: I): ${fullName} { + fromPartial, I>>(${paramName}: I): ${options.useStrictUndefined ? `DeepPartial<${fullName}>` : fullName} { `); } else { chunks.push(code` - fromPartial(${paramName}: ${utils.DeepPartial}<${fullName}>): ${fullName} { + fromPartial(${paramName}: ${utils.DeepPartial}<${fullName}>): ${options.useStrictUndefined ? `DeepPartial<${fullName}>` : fullName} { `); } @@ -2538,12 +2538,20 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const fallback = isWithinOneOf(field) || noDefaultValue ? "undefined" : defaultValue(ctx, field); chunks.push(code`${messageProperty} = ${objectProperty} ?? ${fallback};`); } else { - const fallback = isWithinOneOf(field) || noDefaultValue ? "undefined" : defaultValue(ctx, field); - chunks.push(code` + if(options.useStrictUndefined) { + chunks.push(code` + if (${objectProperty} !== undefined && ${objectProperty} !== null) { + ${messageProperty} = ${readSnippet(`${objectProperty}`)}; + } + `); + } else { + const fallback = isWithinOneOf(field) || noDefaultValue ? "undefined" : defaultValue(ctx, field); + chunks.push(code` ${messageProperty} = (${objectProperty} !== undefined && ${objectProperty} !== null) ? ${readSnippet(`${objectProperty}`)} : ${fallback}; `); + } } }); diff --git a/src/options.ts b/src/options.ts index 55f6d69b9..b46efde15 100644 --- a/src/options.ts +++ b/src/options.ts @@ -104,6 +104,7 @@ export type Options = { annotateFilesWithVersion: boolean; noDefaultsForOptionals: boolean; bigIntLiteral: boolean; + useStrictUndefined: boolean; }; export function defaultOptions(): Options { @@ -172,6 +173,7 @@ export function defaultOptions(): Options { annotateFilesWithVersion: true, noDefaultsForOptionals: false, bigIntLiteral: true, + useStrictUndefined: false }; } diff --git a/src/types.ts b/src/types.ts index 7a487dafd..cf8c72266 100644 --- a/src/types.ts +++ b/src/types.ts @@ -693,7 +693,7 @@ export function toTypeName( ): Code { function finalize(type: Code, isOptional: boolean) { if (isOptional) { - return code`${type} | ${nullOrUndefined(ctx.options, field.proto3Optional)}`; + return code`${type}${ctx.options.useStrictUndefined ? '' : ` | ${nullOrUndefined(ctx.options, field.proto3Optional)}`}`; } return type; } diff --git a/tests/options-test.ts b/tests/options-test.ts index f43cce537..b54f375fa 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -71,6 +71,7 @@ describe("options", () => { "usePrototypeForDefaults": false, "useReadonlyTypes": false, "useSnakeTypeName": true, + "useStrictUndefined": false, } `); });