diff --git a/integration/use-date-false/google/protobuf/timestamp.ts b/integration/use-date-false/google/protobuf/timestamp.ts new file mode 100644 index 000000000..3b5664604 --- /dev/null +++ b/integration/use-date-false/google/protobuf/timestamp.ts @@ -0,0 +1,219 @@ +/* eslint-disable */ +import * as Long from 'long'; +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; + +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 Timestamp { + /** + * 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; +} + +const baseTimestamp: object = { seconds: 0, nanos: 0 }; + +export const Timestamp = { + encode(message: Timestamp, writer: Writer = Writer.create()): Writer { + if (message.seconds !== 0) { + writer.uint32(8).int64(message.seconds); + } + if (message.nanos !== 0) { + writer.uint32(16).int32(message.nanos); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Timestamp { + const reader = input instanceof Uint8Array ? new Reader(input) : input; + let end = length === undefined ? reader.len : reader.pos + length; + const message = globalThis.Object.create(baseTimestamp) as Timestamp; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.seconds = longToNumber(reader.int64() as Long); + break; + case 2: + message.nanos = reader.int32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Timestamp { + const message = globalThis.Object.create(baseTimestamp) as Timestamp; + if (object.seconds !== undefined && object.seconds !== null) { + message.seconds = Number(object.seconds); + } else { + message.seconds = 0; + } + if (object.nanos !== undefined && object.nanos !== null) { + message.nanos = Number(object.nanos); + } else { + message.nanos = 0; + } + return message; + }, + + fromPartial(object: DeepPartial): Timestamp { + const message = { ...baseTimestamp } as Timestamp; + if (object.seconds !== undefined && object.seconds !== null) { + message.seconds = object.seconds; + } else { + message.seconds = 0; + } + if (object.nanos !== undefined && object.nanos !== null) { + message.nanos = object.nanos; + } else { + message.nanos = 0; + } + return message; + }, + + toJSON(message: Timestamp): unknown { + const obj: any = {}; + message.seconds !== undefined && (obj.seconds = message.seconds); + message.nanos !== undefined && (obj.nanos = message.nanos); + return obj; + }, +}; + +declare var self: any | undefined; +declare var window: any | undefined; +var globalThis: any = (() => { + if (typeof globalThis !== 'undefined') return globalThis; + if (typeof self !== 'undefined') return self; + if (typeof window !== 'undefined') return window; + if (typeof global !== 'undefined') return global; + throw 'Unable to locate global object'; +})(); + +type Builtin = Date | Function | Uint8Array | string | number | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +function longToNumber(long: Long): number { + if (long.gt(Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER'); + } + return long.toNumber(); +} + +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/use-date-false/metadata.bin b/integration/use-date-false/metadata.bin new file mode 100644 index 000000000..cebb47a0f Binary files /dev/null and b/integration/use-date-false/metadata.bin differ diff --git a/integration/use-date-false/metadata.proto b/integration/use-date-false/metadata.proto new file mode 100644 index 000000000..96ba61b71 --- /dev/null +++ b/integration/use-date-false/metadata.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +message Metadata { + google.protobuf.Timestamp last_edited = 1; +} \ No newline at end of file diff --git a/integration/use-date-false/metadata.ts b/integration/use-date-false/metadata.ts new file mode 100644 index 000000000..d3e38244a --- /dev/null +++ b/integration/use-date-false/metadata.ts @@ -0,0 +1,108 @@ +/* eslint-disable */ +import { Timestamp } from './google/protobuf/timestamp'; +import { Writer, Reader } from 'protobufjs/minimal'; + +export const protobufPackage = ''; + +export interface Metadata { + lastEdited: Timestamp | undefined; +} + +const baseMetadata: object = {}; + +export const Metadata = { + encode(message: Metadata, writer: Writer = Writer.create()): Writer { + if (message.lastEdited !== undefined) { + Timestamp.encode(message.lastEdited, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Metadata { + const reader = input instanceof Uint8Array ? new Reader(input) : input; + let end = length === undefined ? reader.len : reader.pos + length; + const message = globalThis.Object.create(baseMetadata) as Metadata; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.lastEdited = Timestamp.decode(reader, reader.uint32()); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Metadata { + const message = globalThis.Object.create(baseMetadata) as Metadata; + if (object.lastEdited !== undefined && object.lastEdited !== null) { + message.lastEdited = fromJsonTimestamp(object.lastEdited); + } else { + message.lastEdited = undefined; + } + return message; + }, + + fromPartial(object: DeepPartial): Metadata { + const message = { ...baseMetadata } as Metadata; + if (object.lastEdited !== undefined && object.lastEdited !== null) { + message.lastEdited = Timestamp.fromPartial(object.lastEdited); + } else { + message.lastEdited = undefined; + } + return message; + }, + + toJSON(message: Metadata): unknown { + const obj: any = {}; + message.lastEdited !== undefined && + (obj.lastEdited = message.lastEdited !== undefined ? fromTimestamp(message.lastEdited).toISOString() : null); + return obj; + }, +}; + +declare var self: any | undefined; +declare var window: any | undefined; +var globalThis: any = (() => { + if (typeof globalThis !== 'undefined') return globalThis; + if (typeof self !== 'undefined') return self; + if (typeof window !== 'undefined') return window; + if (typeof global !== 'undefined') return global; + throw 'Unable to locate global object'; +})(); + +type Builtin = Date | Function | Uint8Array | string | number | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +function toTimestamp(date: Date): Timestamp { + const seconds = date.getTime() / 1_000; + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = t.seconds * 1_000; + millis += t.nanos / 1_000_000; + return new Date(millis); +} + +function fromJsonTimestamp(o: any): Timestamp { + if (o instanceof Date) { + return toTimestamp(o); + } else if (typeof o === 'string') { + return toTimestamp(new Date(o)); + } else { + return Timestamp.fromJSON(o); + } +} diff --git a/integration/use-date-false/parameters.txt b/integration/use-date-false/parameters.txt new file mode 100644 index 000000000..0a0d69154 --- /dev/null +++ b/integration/use-date-false/parameters.txt @@ -0,0 +1 @@ +useDate=false \ No newline at end of file diff --git a/integration/use-date-false/use-date-test.ts b/integration/use-date-false/use-date-test.ts new file mode 100644 index 000000000..b949f31f7 --- /dev/null +++ b/integration/use-date-false/use-date-test.ts @@ -0,0 +1,13 @@ +import { Metadata } from './metadata'; + +describe('useDate=false', () => { + it('generates types that compile and encode', () => { + const output = Metadata.encode({ + lastEdited: { + seconds: 123456789, + nanos: 234567890, + } + }).finish(); + expect(output.length).toBeGreaterThan(8); + }); +}); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 3e5ca8fd9..fa185363a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -400,7 +400,8 @@ function makeTimestampMethods(options: Options, longs: ReturnType>> 0; writeSnippet = (place) => code`writer.uint32(${tag}).${toReaderCall(field)}(${place})`; - } else if (isTimestamp(field)) { + } else if (isTimestamp(field) && options.useDate) { const tag = ((field.number << 3) | 2) >>> 0; const type = basicTypeName(ctx, field, { keepValueType: true }); writeSnippet = (place) => @@ -887,7 +899,9 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP ? code`${from} !== undefined ? ${toJson}(${from}) : undefined` : code`${toJson}(${from})`; } else if (isTimestamp(field)) { - return code`${from} !== undefined ? ${from}.toISOString() : null`; + return options.useDate + ? code`${from} !== undefined ? ${from}.toISOString() : null` + : code`${from} !== undefined ? ${utils.fromTimestamp}(${from}).toISOString() : null`; } else if (isMapType(ctx, messageDesc, field)) { // For map types, drill-in and then admittedly re-hard-code our per-value-type logic const valueType = (typeMap.get(field.typeName)![2] as DescriptorProto).field[1]; @@ -897,7 +911,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP } else if (isBytes(valueType)) { return code`${utils.base64FromBytes}(${from})`; } else if (isTimestamp(valueType)) { - return code`${from}.toISOString()`; + return options.useDate ? code`${from}.toISOString()` : code`${utils.fromTimestamp}(${from}).toISOString()`; } else if (isScalar(valueType)) { return code`${from}`; } else { @@ -978,7 +992,9 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const fieldName = maybeSnakeToCamel(field.name, options); const readSnippet = (from: string): Code => { - if (isPrimitive(field) || isTimestamp(field) || isValueType(ctx, field)) { + if (isPrimitive(field) || isValueType(ctx, field)) { + return code`${from}`; + } else if (isTimestamp(field) && options.useDate) { return code`${from}`; } else if (isMessage(field)) { if (isRepeated(field) && isMapType(ctx, messageDesc, field)) { @@ -992,7 +1008,7 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const cstr = capitalize(basicTypeName(ctx, valueType).toCodeString()); return code`${cstr}(${from})`; } - } else if (isTimestamp(valueType)) { + } else if (isTimestamp(valueType) && options.useDate) { return code`${from}`; } else { const type = basicTypeName(ctx, valueType); @@ -1015,7 +1031,7 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri chunks.push(code` Object.entries(object.${fieldName}).forEach(([key, value]) => { if (value !== undefined) { - message.${fieldName}[${i}] = ${readSnippet('value')}; + message.${fieldName}[${i}] = ${readSnippet('value')}; } }); `);