diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3feb2d0..6b5dbfe4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.2.2](https://github.com/stephenh/ts-proto/compare/v2.2.1...v2.2.2) (2024-10-04) + + +### Bug Fixes + +* prefix and suffixes were not being applied to to/fromTimestamp resulting in compile error ([#1118](https://github.com/stephenh/ts-proto/issues/1118)) ([22c2905](https://github.com/stephenh/ts-proto/commit/22c2905ca53c88bdb2802386d414d584a451aa4c)) + ## [2.2.1](https://github.com/stephenh/ts-proto/compare/v2.2.0...v2.2.1) (2024-09-29) diff --git a/integration/affixes/affixes.proto b/integration/affixes/affixes.proto index 0b40d8720..2c2c936ee 100644 --- a/integration/affixes/affixes.proto +++ b/integration/affixes/affixes.proto @@ -1,4 +1,6 @@ syntax = "proto3"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/timestamp.proto"; package affixes; @@ -15,3 +17,8 @@ enum AwesomeEnum { service AwesomeService { } + +message Google { + google.protobuf.UInt64Value guint64 = 13; + google.protobuf.Timestamp timestamp = 14; +} diff --git a/integration/affixes/affixes.ts b/integration/affixes/affixes.ts index 593b4853b..42221e3f8 100644 --- a/integration/affixes/affixes.ts +++ b/integration/affixes/affixes.ts @@ -3,6 +3,8 @@ /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import { PrefixTimestampSuffix } from "./google/protobuf/timestamp"; +import { PrefixUInt64ValueSuffix } from "./google/protobuf/wrappers"; export const protobufPackage = "affixes"; @@ -51,6 +53,11 @@ export interface PrefixAwesomeMessageSuffix { export interface PrefixAwesomeMessage_InnerSuffix { } +export interface PrefixGoogleSuffix { + guint64: number | undefined; + timestamp: Date | undefined; +} + function createBasePrefixAwesomeMessageSuffix(): PrefixAwesomeMessageSuffix { return {}; } @@ -141,6 +148,82 @@ export const PrefixAwesomeMessage_InnerSuffix: MessageFns = { + encode(message: PrefixGoogleSuffix, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.guint64 !== undefined) { + PrefixUInt64ValueSuffix.encode({ value: message.guint64! }, writer.uint32(106).fork()).join(); + } + if (message.timestamp !== undefined) { + PrefixTimestampSuffix.encode(toTimestamp(message.timestamp), writer.uint32(114).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PrefixGoogleSuffix { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePrefixGoogleSuffix(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 13: { + if (tag !== 106) { + break; + } + + message.guint64 = PrefixUInt64ValueSuffix.decode(reader, reader.uint32()).value; + continue; + } + case 14: { + if (tag !== 114) { + break; + } + + message.timestamp = fromTimestamp(PrefixTimestampSuffix.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): PrefixGoogleSuffix { + return { + guint64: isSet(object.guint64) ? Number(object.guint64) : undefined, + timestamp: isSet(object.timestamp) ? fromJsonTimestamp(object.timestamp) : undefined, + }; + }, + + toJSON(message: PrefixGoogleSuffix): unknown { + const obj: any = {}; + if (message.guint64 !== undefined) { + obj.guint64 = message.guint64; + } + if (message.timestamp !== undefined) { + obj.timestamp = message.timestamp.toISOString(); + } + return obj; + }, + + create, I>>(base?: I): PrefixGoogleSuffix { + return PrefixGoogleSuffix.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): PrefixGoogleSuffix { + const message = createBasePrefixGoogleSuffix(); + message.guint64 = object.guint64 ?? undefined; + message.timestamp = object.timestamp ?? undefined; + return message; + }, +}; + export interface AwesomeService { } @@ -170,6 +253,32 @@ 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 toTimestamp(date: Date): PrefixTimestampSuffix { + const seconds = Math.trunc(date.getTime() / 1_000); + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: PrefixTimestampSuffix): Date { + let millis = (t.seconds || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new globalThis.Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof globalThis.Date) { + return o; + } else if (typeof o === "string") { + return new globalThis.Date(o); + } else { + return fromTimestamp(PrefixTimestampSuffix.fromJSON(o)); + } +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; diff --git a/integration/suffix-type/google/protobuf/timestamp.ts b/integration/suffix-type/google/protobuf/timestamp.ts new file mode 100644 index 000000000..0d92e5530 --- /dev/null +++ b/integration/suffix-type/google/protobuf/timestamp.ts @@ -0,0 +1,226 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// source: google/protobuf/timestamp.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "google.protobuf"; + +/** + * A Timestamp represents a point in time independent of any time zone or local + * calendar, encoded as a count of seconds and fractions of seconds at + * nanosecond resolution. The count is relative to an epoch at UTC midnight on + * January 1, 1970, in the proleptic Gregorian calendar which extends the + * Gregorian calendar backwards to year one. + * + * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + * second table is needed for interpretation, using a [24-hour linear + * smear](https://developers.google.com/time/smear). + * + * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + * restricting to that range, we ensure that we can convert to and from [RFC + * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + * + * # Examples + * + * Example 1: Compute Timestamp from POSIX `time()`. + * + * Timestamp timestamp; + * timestamp.set_seconds(time(NULL)); + * timestamp.set_nanos(0); + * + * Example 2: Compute Timestamp from POSIX `gettimeofday()`. + * + * struct timeval tv; + * gettimeofday(&tv, NULL); + * + * Timestamp timestamp; + * timestamp.set_seconds(tv.tv_sec); + * timestamp.set_nanos(tv.tv_usec * 1000); + * + * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + * + * FILETIME ft; + * GetSystemTimeAsFileTime(&ft); + * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + * + * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + * Timestamp timestamp; + * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + * + * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + * + * long millis = System.currentTimeMillis(); + * + * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + * .setNanos((int) ((millis % 1000) * 1000000)).build(); + * + * Example 5: Compute Timestamp from Java `Instant.now()`. + * + * Instant now = Instant.now(); + * + * Timestamp timestamp = + * Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + * .setNanos(now.getNano()).build(); + * + * Example 6: Compute Timestamp from current time in Python. + * + * timestamp = Timestamp() + * timestamp.GetCurrentTime() + * + * # JSON Mapping + * + * In JSON format, the Timestamp type is encoded as a string in the + * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + * where {year} is always expressed using four digits while {month}, {day}, + * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + * is required. A proto3 JSON serializer should always use UTC (as indicated by + * "Z") when printing the Timestamp type and a proto3 JSON parser should be + * able to accept both UTC and other timezones (as indicated by an offset). + * + * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + * 01:30 UTC on January 15, 2017. + * + * In JavaScript, one can convert a Date object to this format using the + * standard + * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + * method. In Python, a standard `datetime.datetime` object can be converted + * to this format using + * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + * the Joda Time's [`ISODateTimeFormat.dateTime()`]( + * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + * ) to obtain a formatter capable of generating timestamps in this format. + */ +export interface GRPCPTimestampGRPCS { + /** + * Represents seconds of UTC time since Unix epoch + * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59Z inclusive. + */ + seconds: number; + /** + * Non-negative fractions of a second at nanosecond resolution. Negative + * second values with fractions must still have non-negative nanos values + * that count forward in time. Must be from 0 to 999,999,999 + * inclusive. + */ + nanos: number; +} + +function createBaseGRPCPTimestampGRPCS(): GRPCPTimestampGRPCS { + return { seconds: 0, nanos: 0 }; +} + +export const GRPCPTimestampGRPCS: MessageFns = { + encode(message: GRPCPTimestampGRPCS, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.seconds !== 0) { + writer.uint32(8).int64(message.seconds); + } + if (message.nanos !== 0) { + writer.uint32(16).int32(message.nanos); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GRPCPTimestampGRPCS { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGRPCPTimestampGRPCS(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.seconds = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.nanos = reader.int32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GRPCPTimestampGRPCS { + return { + seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0, + nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0, + }; + }, + + toJSON(message: GRPCPTimestampGRPCS): unknown { + const obj: any = {}; + if (message.seconds !== 0) { + obj.seconds = Math.round(message.seconds); + } + if (message.nanos !== 0) { + obj.nanos = Math.round(message.nanos); + } + return obj; + }, + + create, I>>(base?: I): GRPCPTimestampGRPCS { + return GRPCPTimestampGRPCS.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GRPCPTimestampGRPCS { + const message = createBaseGRPCPTimestampGRPCS(); + message.seconds = object.seconds ?? 0; + message.nanos = object.nanos ?? 0; + 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 longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/integration/suffix-type/parameters.txt b/integration/suffix-type/parameters.txt new file mode 100644 index 000000000..369acbcc5 --- /dev/null +++ b/integration/suffix-type/parameters.txt @@ -0,0 +1 @@ +typeSuffix=GRPCS,typePrefix=GRPCP diff --git a/integration/suffix-type/suffix-test.ts b/integration/suffix-type/suffix-test.ts new file mode 100644 index 000000000..c57fb97dd --- /dev/null +++ b/integration/suffix-type/suffix-test.ts @@ -0,0 +1,18 @@ +import { GRPCPSuffixTypeGRPCS } from "./suffix-type"; + +describe("suffix", () => { + it("generates types correctly", () => { + const obj: GRPCPSuffixTypeGRPCS = { + createdAt: new Date("2011-10-05T14:48:00.000Z"), + }; + expect(obj).toBeTruthy(); + + // make sure all conversions to and from handle prefixes and suffixes + // + const json = GRPCPSuffixTypeGRPCS.toJSON(obj); + + expect(json).toEqual({ createdAt: "2011-10-05T14:48:00.000Z" }); + + expect(GRPCPSuffixTypeGRPCS.fromJSON(json)).toEqual(obj); + }); +}); diff --git a/integration/suffix-type/suffix-type.proto b/integration/suffix-type/suffix-type.proto new file mode 100644 index 000000000..b6242f8ce --- /dev/null +++ b/integration/suffix-type/suffix-type.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; +import "google/protobuf/timestamp.proto"; + +message SuffixType { + google.protobuf.Timestamp created_at = 9; +} diff --git a/integration/suffix-type/suffix-type.ts b/integration/suffix-type/suffix-type.ts new file mode 100644 index 000000000..943c8a51c --- /dev/null +++ b/integration/suffix-type/suffix-type.ts @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// source: suffix-type.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; +import { GRPCPTimestampGRPCS } from "./google/protobuf/timestamp"; + +export const protobufPackage = ""; + +export interface GRPCPSuffixTypeGRPCS { + createdAt: Date | undefined; +} + +function createBaseGRPCPSuffixTypeGRPCS(): GRPCPSuffixTypeGRPCS { + return { createdAt: undefined }; +} + +export const GRPCPSuffixTypeGRPCS: MessageFns = { + encode(message: GRPCPSuffixTypeGRPCS, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.createdAt !== undefined) { + GRPCPTimestampGRPCS.encode(toTimestamp(message.createdAt), writer.uint32(74).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GRPCPSuffixTypeGRPCS { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGRPCPSuffixTypeGRPCS(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 9: { + if (tag !== 74) { + break; + } + + message.createdAt = fromTimestamp(GRPCPTimestampGRPCS.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GRPCPSuffixTypeGRPCS { + return { createdAt: isSet(object.createdAt) ? fromJsonTimestamp(object.createdAt) : undefined }; + }, + + toJSON(message: GRPCPSuffixTypeGRPCS): unknown { + const obj: any = {}; + if (message.createdAt !== undefined) { + obj.createdAt = message.createdAt.toISOString(); + } + return obj; + }, + + create, I>>(base?: I): GRPCPSuffixTypeGRPCS { + return GRPCPSuffixTypeGRPCS.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GRPCPSuffixTypeGRPCS { + const message = createBaseGRPCPSuffixTypeGRPCS(); + message.createdAt = object.createdAt ?? 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 toTimestamp(date: Date): GRPCPTimestampGRPCS { + const seconds = Math.trunc(date.getTime() / 1_000); + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: GRPCPTimestampGRPCS): Date { + let millis = (t.seconds || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new globalThis.Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof globalThis.Date) { + return o; + } else if (typeof o === "string") { + return new globalThis.Date(o); + } else { + return fromTimestamp(GRPCPTimestampGRPCS.fromJSON(o)); + } +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + fromJSON(object: any): T; + toJSON(message: T): unknown; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/package.json b/package.json index 0696179a1..1348bffa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-proto", - "version": "2.2.1", + "version": "2.2.2", "description": "", "main": "build/src/plugin.js", "repository": "github:stephenh/ts-proto", diff --git a/src/encode.ts b/src/encode.ts index 41ff98143..4241521a2 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -70,7 +70,7 @@ export function generateDecoder(ctx: Context, typeName: string): Code { let TypeValue: Import; if (name == "Timestamp") { - TypeValue = impProto(ctx.options, "google/protobuf/timestamp", name); + TypeValue = impProto(ctx.options, "google/protobuf/timestamp", options.typePrefix + name + options.typeSuffix); const decoder = code`${TypeValue}.decode(value)`; if ( diff --git a/src/main.ts b/src/main.ts index 3702505c9..77e9a9dd6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -906,7 +906,11 @@ function makeTimestampMethods( longs: ReturnType, bytes: ReturnType, ) { - const Timestamp = impProto(options, "google/protobuf/timestamp", "Timestamp"); + const Timestamp = impProto( + options, + "google/protobuf/timestamp", + `${options.typePrefix}Timestamp${options.typeSuffix}`, + ); const NanoDate = imp("NanoDate=nano-date"); let seconds: string | Code = "Math.trunc(date.getTime() / 1_000)"; @@ -1009,12 +1013,12 @@ function makeTimestampMethods( } else if (typeof o === "string") { return new ${bytes.globalThis}.Date(o); } else { - return ${fromTimestamp}(Timestamp.fromJSON(o)); + return ${fromTimestamp}(${options.typePrefix}Timestamp${options.typeSuffix}.fromJSON(o)); } } ` : code` - function fromJsonTimestamp(o: any): Timestamp { + function fromJsonTimestamp(o: any): ${options.typePrefix}Timestamp${options.typeSuffix} { if (o instanceof ${bytes.globalThis}.Date) { return ${toTimestamp}(o); } else if (typeof o === "string") {