diff --git a/integration/use-optionals-deprecated-only/optionals-test.ts b/integration/use-optionals-deprecated-only/optionals-test.ts new file mode 100644 index 000000000..3f151e3db --- /dev/null +++ b/integration/use-optionals-deprecated-only/optionals-test.ts @@ -0,0 +1,56 @@ +import { OptionalsTest, StateEnum } from './test' + +describe('useOptionals=deprecatedOnly', () => { + it('has deprecated fields optional members', () => { + const test: OptionalsTest = { + id: 1, + long: 10, + + repId: [1, 2], + repState: [StateEnum.ON, StateEnum.OFF], + repLong: [11, 12], + repTruth: [true, false], + repDescription: ["hello", "world"], + repData: [Buffer.alloc(3).fill(0x33), Buffer.alloc(4).fill(0x34), Buffer.alloc(5).fill(0x35)], + + optId: 2, + optLong: 13, + optTruth: true, + optDescription: "mumble", + optData: Buffer.alloc(6).fill(0x36), + + translations: { + "hello": "hallo", + "world": "wereld", + }, + }; + const data = OptionalsTest.encode(test).finish(); + const test2 = OptionalsTest.decode(data); + expect(test2).toEqual({ + id: 1, + state: StateEnum.UNKNOWN, + long: 10, + truth: false, + description: "", + data: new Uint8Array(0), + + repId: [1, 2], + repState: [StateEnum.ON, StateEnum.OFF], + repLong: [11, 12], + repTruth: [true, false], + repDescription: ["hello", "world"], + repData: [Buffer.alloc(3).fill(0x33), Buffer.alloc(4).fill(0x34), Buffer.alloc(5).fill(0x35)], + + optId: 2, + optLong: 13, + optTruth: true, + optDescription: "mumble", + optData: Buffer.alloc(6).fill(0x36), + + translations: { + "hello": "hallo", + "world": "wereld", + }, + }); + }); +}) diff --git a/integration/use-optionals-deprecated-only/parameters.txt b/integration/use-optionals-deprecated-only/parameters.txt new file mode 100644 index 000000000..061f75b8a --- /dev/null +++ b/integration/use-optionals-deprecated-only/parameters.txt @@ -0,0 +1 @@ +useOptionals=deprecatedOnly \ No newline at end of file diff --git a/integration/use-optionals-deprecated-only/test.bin b/integration/use-optionals-deprecated-only/test.bin new file mode 100644 index 000000000..5cd4cc7af Binary files /dev/null and b/integration/use-optionals-deprecated-only/test.bin differ diff --git a/integration/use-optionals-deprecated-only/test.proto b/integration/use-optionals-deprecated-only/test.proto new file mode 100644 index 000000000..6c1e757a2 --- /dev/null +++ b/integration/use-optionals-deprecated-only/test.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; +package optionalstest; + +message OptionalsTest { + int32 id = 1; + StateEnum state = 2 [deprecated = true]; + int64 long = 3; + bool truth = 4 [deprecated = true]; + string description = 5 [deprecated = true]; + bytes data = 6 [deprecated = true]; + + repeated int32 rep_id = 7; + repeated StateEnum rep_state = 8 [deprecated = true]; + repeated int64 rep_long = 9; + repeated bool rep_truth = 10; + repeated string rep_description = 11; + repeated bytes rep_data = 12; + + optional int32 opt_id = 13; + optional StateEnum opt_state = 14 [deprecated = true]; + optional int64 opt_long = 15; + optional bool opt_truth = 16; + optional string opt_description = 17; + optional bytes opt_data = 18; + + map translations = 19; +} + +enum StateEnum { + option deprecated = true; + UNKNOWN = 0; + ON = 1; + OFF = 2; +} diff --git a/integration/use-optionals-deprecated-only/test.ts b/integration/use-optionals-deprecated-only/test.ts new file mode 100644 index 000000000..17c2174bb --- /dev/null +++ b/integration/use-optionals-deprecated-only/test.ts @@ -0,0 +1,640 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; +import Long = require("long"); + +export const protobufPackage = "optionalstest"; + +/** @deprecated */ +export enum StateEnum { + UNKNOWN = 0, + ON = 1, + OFF = 2, + UNRECOGNIZED = -1, +} + +export function stateEnumFromJSON(object: any): StateEnum { + switch (object) { + case 0: + case "UNKNOWN": + return StateEnum.UNKNOWN; + case 1: + case "ON": + return StateEnum.ON; + case 2: + case "OFF": + return StateEnum.OFF; + case -1: + case "UNRECOGNIZED": + default: + return StateEnum.UNRECOGNIZED; + } +} + +export function stateEnumToJSON(object: StateEnum): string { + switch (object) { + case StateEnum.UNKNOWN: + return "UNKNOWN"; + case StateEnum.ON: + return "ON"; + case StateEnum.OFF: + return "OFF"; + case StateEnum.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export interface OptionalsTest { + id: number; + /** @deprecated */ + state?: StateEnum | undefined; + long: number; + /** @deprecated */ + truth?: + | boolean + | undefined; + /** @deprecated */ + description?: + | string + | undefined; + /** @deprecated */ + data?: Uint8Array | undefined; + repId: number[]; + /** @deprecated */ + repState: StateEnum[]; + repLong: number[]; + repTruth: boolean[]; + repDescription: string[]; + repData: Uint8Array[]; + optId?: + | number + | undefined; + /** @deprecated */ + optState?: StateEnum | undefined; + optLong?: number | undefined; + optTruth?: boolean | undefined; + optDescription?: string | undefined; + optData?: Uint8Array | undefined; + translations: { [key: string]: string }; +} + +export interface OptionalsTest_TranslationsEntry { + key: string; + value: string; +} + +function createBaseOptionalsTest(): OptionalsTest { + return { + id: 0, + state: 0, + long: 0, + truth: false, + description: "", + data: new Uint8Array(0), + repId: [], + repState: [], + repLong: [], + repTruth: [], + repDescription: [], + repData: [], + optId: undefined, + optState: undefined, + optLong: undefined, + optTruth: undefined, + optDescription: undefined, + optData: undefined, + translations: {}, + }; +} + +export const OptionalsTest = { + encode(message: OptionalsTest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== 0) { + writer.uint32(8).int32(message.id); + } + if (message.state !== undefined && message.state !== 0) { + writer.uint32(16).int32(message.state); + } + if (message.long !== 0) { + writer.uint32(24).int64(message.long); + } + if (message.truth === true) { + writer.uint32(32).bool(message.truth); + } + if (message.description !== undefined && message.description !== "") { + writer.uint32(42).string(message.description); + } + if (message.data !== undefined && message.data.length !== 0) { + writer.uint32(50).bytes(message.data); + } + writer.uint32(58).fork(); + for (const v of message.repId) { + writer.int32(v); + } + writer.ldelim(); + writer.uint32(66).fork(); + for (const v of message.repState) { + writer.int32(v); + } + writer.ldelim(); + writer.uint32(74).fork(); + for (const v of message.repLong) { + writer.int64(v); + } + writer.ldelim(); + writer.uint32(82).fork(); + for (const v of message.repTruth) { + writer.bool(v); + } + writer.ldelim(); + for (const v of message.repDescription) { + writer.uint32(90).string(v!); + } + for (const v of message.repData) { + writer.uint32(98).bytes(v!); + } + if (message.optId !== undefined) { + writer.uint32(104).int32(message.optId); + } + if (message.optState !== undefined) { + writer.uint32(112).int32(message.optState); + } + if (message.optLong !== undefined) { + writer.uint32(120).int64(message.optLong); + } + if (message.optTruth !== undefined) { + writer.uint32(128).bool(message.optTruth); + } + if (message.optDescription !== undefined) { + writer.uint32(138).string(message.optDescription); + } + if (message.optData !== undefined) { + writer.uint32(146).bytes(message.optData); + } + Object.entries(message.translations).forEach(([key, value]) => { + OptionalsTest_TranslationsEntry.encode({ key: key as any, value }, writer.uint32(154).fork()).ldelim(); + }); + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): OptionalsTest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseOptionalsTest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.id = reader.int32(); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.state = reader.int32() as any; + continue; + case 3: + if (tag !== 24) { + break; + } + + message.long = longToNumber(reader.int64() as Long); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.truth = reader.bool(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.description = reader.string(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.data = reader.bytes(); + continue; + case 7: + if (tag === 56) { + message.repId.push(reader.int32()); + + continue; + } + + if (tag === 58) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.repId.push(reader.int32()); + } + + continue; + } + + break; + case 8: + if (tag === 64) { + message.repState.push(reader.int32() as any); + + continue; + } + + if (tag === 66) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.repState.push(reader.int32() as any); + } + + continue; + } + + break; + case 9: + if (tag === 72) { + message.repLong.push(longToNumber(reader.int64() as Long)); + + continue; + } + + if (tag === 74) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.repLong.push(longToNumber(reader.int64() as Long)); + } + + continue; + } + + break; + case 10: + if (tag === 80) { + message.repTruth.push(reader.bool()); + + continue; + } + + if (tag === 82) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.repTruth.push(reader.bool()); + } + + continue; + } + + break; + case 11: + if (tag !== 90) { + break; + } + + message.repDescription.push(reader.string()); + continue; + case 12: + if (tag !== 98) { + break; + } + + message.repData.push(reader.bytes()); + continue; + case 13: + if (tag !== 104) { + break; + } + + message.optId = reader.int32(); + continue; + case 14: + if (tag !== 112) { + break; + } + + message.optState = reader.int32() as any; + continue; + case 15: + if (tag !== 120) { + break; + } + + message.optLong = longToNumber(reader.int64() as Long); + continue; + case 16: + if (tag !== 128) { + break; + } + + message.optTruth = reader.bool(); + continue; + case 17: + if (tag !== 138) { + break; + } + + message.optDescription = reader.string(); + continue; + case 18: + if (tag !== 146) { + break; + } + + message.optData = reader.bytes(); + continue; + case 19: + if (tag !== 154) { + break; + } + + const entry19 = OptionalsTest_TranslationsEntry.decode(reader, reader.uint32()); + if (entry19.value !== undefined) { + message.translations[entry19.key] = entry19.value; + } + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): OptionalsTest { + return { + id: isSet(object.id) ? globalThis.Number(object.id) : 0, + state: isSet(object.state) ? stateEnumFromJSON(object.state) : 0, + long: isSet(object.long) ? globalThis.Number(object.long) : 0, + truth: isSet(object.truth) ? globalThis.Boolean(object.truth) : false, + description: isSet(object.description) ? globalThis.String(object.description) : "", + data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), + repId: globalThis.Array.isArray(object?.repId) ? object.repId.map((e: any) => globalThis.Number(e)) : [], + repState: globalThis.Array.isArray(object?.repState) ? object.repState.map((e: any) => stateEnumFromJSON(e)) : [], + repLong: globalThis.Array.isArray(object?.repLong) ? object.repLong.map((e: any) => globalThis.Number(e)) : [], + repTruth: globalThis.Array.isArray(object?.repTruth) + ? object.repTruth.map((e: any) => globalThis.Boolean(e)) + : [], + repDescription: globalThis.Array.isArray(object?.repDescription) + ? object.repDescription.map((e: any) => globalThis.String(e)) + : [], + repData: globalThis.Array.isArray(object?.repData) ? object.repData.map((e: any) => bytesFromBase64(e)) : [], + optId: isSet(object.optId) ? globalThis.Number(object.optId) : undefined, + optState: isSet(object.optState) ? stateEnumFromJSON(object.optState) : undefined, + optLong: isSet(object.optLong) ? globalThis.Number(object.optLong) : undefined, + optTruth: isSet(object.optTruth) ? globalThis.Boolean(object.optTruth) : undefined, + optDescription: isSet(object.optDescription) ? globalThis.String(object.optDescription) : undefined, + optData: isSet(object.optData) ? bytesFromBase64(object.optData) : undefined, + translations: isObject(object.translations) + ? Object.entries(object.translations).reduce<{ [key: string]: string }>((acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, {}) + : {}, + }; + }, + + toJSON(message: OptionalsTest): unknown { + const obj: any = {}; + if (message.id !== 0) { + obj.id = Math.round(message.id); + } + if (message.state !== undefined && message.state !== 0) { + obj.state = stateEnumToJSON(message.state); + } + if (message.long !== 0) { + obj.long = Math.round(message.long); + } + if (message.truth === true) { + obj.truth = message.truth; + } + if (message.description !== undefined && message.description !== "") { + obj.description = message.description; + } + if (message.data !== undefined && message.data.length !== 0) { + obj.data = base64FromBytes(message.data); + } + if (message.repId?.length) { + obj.repId = message.repId.map((e) => Math.round(e)); + } + if (message.repState?.length) { + obj.repState = message.repState.map((e) => stateEnumToJSON(e)); + } + if (message.repLong?.length) { + obj.repLong = message.repLong.map((e) => Math.round(e)); + } + if (message.repTruth?.length) { + obj.repTruth = message.repTruth; + } + if (message.repDescription?.length) { + obj.repDescription = message.repDescription; + } + if (message.repData?.length) { + obj.repData = message.repData.map((e) => base64FromBytes(e)); + } + if (message.optId !== undefined) { + obj.optId = Math.round(message.optId); + } + if (message.optState !== undefined) { + obj.optState = stateEnumToJSON(message.optState); + } + if (message.optLong !== undefined) { + obj.optLong = Math.round(message.optLong); + } + if (message.optTruth !== undefined) { + obj.optTruth = message.optTruth; + } + if (message.optDescription !== undefined) { + obj.optDescription = message.optDescription; + } + if (message.optData !== undefined) { + obj.optData = base64FromBytes(message.optData); + } + if (message.translations) { + const entries = Object.entries(message.translations); + if (entries.length > 0) { + obj.translations = {}; + entries.forEach(([k, v]) => { + obj.translations[k] = v; + }); + } + } + return obj; + }, + + create, I>>(base?: I): OptionalsTest { + return OptionalsTest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): OptionalsTest { + const message = createBaseOptionalsTest(); + message.id = object.id ?? 0; + message.state = object.state ?? 0; + message.long = object.long ?? 0; + message.truth = object.truth ?? false; + message.description = object.description ?? ""; + message.data = object.data ?? new Uint8Array(0); + message.repId = object.repId?.map((e) => e) || []; + message.repState = object.repState?.map((e) => e) || []; + message.repLong = object.repLong?.map((e) => e) || []; + message.repTruth = object.repTruth?.map((e) => e) || []; + message.repDescription = object.repDescription?.map((e) => e) || []; + message.repData = object.repData?.map((e) => e) || []; + message.optId = object.optId ?? undefined; + message.optState = object.optState ?? undefined; + message.optLong = object.optLong ?? undefined; + message.optTruth = object.optTruth ?? undefined; + message.optDescription = object.optDescription ?? undefined; + message.optData = object.optData ?? undefined; + message.translations = Object.entries(object.translations ?? {}).reduce<{ [key: string]: string }>( + (acc, [key, value]) => { + if (value !== undefined) { + acc[key] = globalThis.String(value); + } + return acc; + }, + {}, + ); + return message; + }, +}; + +function createBaseOptionalsTest_TranslationsEntry(): OptionalsTest_TranslationsEntry { + return { key: "", value: "" }; +} + +export const OptionalsTest_TranslationsEntry = { + encode(message: OptionalsTest_TranslationsEntry, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): OptionalsTest_TranslationsEntry { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseOptionalsTest_TranslationsEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): OptionalsTest_TranslationsEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + }; + }, + + toJSON(message: OptionalsTest_TranslationsEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== "") { + obj.value = message.value; + } + return obj; + }, + + create, I>>(base?: I): OptionalsTest_TranslationsEntry { + return OptionalsTest_TranslationsEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): OptionalsTest_TranslationsEntry { + const message = createBaseOptionalsTest_TranslationsEntry(); + message.key = object.key ?? ""; + message.value = object.value ?? ""; + return message; + }, +}; + +function bytesFromBase64(b64: string): Uint8Array { + if ((globalThis as any).Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if ((globalThis as any).Buffer) { + return globalThis.Buffer.from(arr).toString("base64"); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join("")); + } +} + +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 longToNumber(long: Long): number { + if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + return long.toNumber(); +} + +if (_m0.util.Long !== Long) { + _m0.util.Long = Long as any; + _m0.configure(); +} + +function isObject(value: any): boolean { + return typeof value === "object" && value !== null; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/src/options.ts b/src/options.ts index 11d849d3c..4eae9ed4f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -43,7 +43,7 @@ export type Options = { snakeToCamel: Array<"json" | "keys">; forceLong: LongOption; globalThisPolyfill: boolean; - useOptionals: boolean | "none" | "messages" | "all"; // boolean is deprecated + useOptionals: boolean | "none" | "deprecatedOnly" | "messages" | "all"; // boolean is deprecated emitDefaultValues: Array<"json-methods">; useDate: DateOption; useJsonTimestamp: JsonTimestampOption; diff --git a/src/types.ts b/src/types.ts index 576150cd1..c94f290a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -348,6 +348,7 @@ export function isScalar(field: FieldDescriptorProto): boolean { return scalarTypes.includes(field.type); } +// When useOptionals='deprecatedOnly', all deprecated fields are translated into optional // When useOptionals='messages', non-scalar fields are translated into optional // properties. When useOptionals='all', all fields are translated into // optional properties, with the exception of map Entry key/values, which must @@ -361,9 +362,12 @@ export function isOptionalProperty( const optionalMessages = options.useOptionals === true || options.useOptionals === "messages" || options.useOptionals === "all"; const optionalAll = options.useOptionals === "all"; + const deprecatedOnly = options.useOptionals === "deprecatedOnly" && field.options && field.options.deprecated; + return ( (optionalMessages && isMessage(field) && !isRepeated(field)) || (optionalAll && !messageOptions?.mapEntry) || + (deprecatedOnly && !isMessage(field) && !isRepeated(field)) || // don't bother verifying that oneof is not union. union oneofs generate their own properties. isWithinOneOf(field) || field.proto3Optional