Skip to content

Commit

Permalink
Only change codegen if useOptionals is messages/all.
Browse files Browse the repository at this point in the history
  • Loading branch information
sgielen committed Dec 10, 2021
1 parent a4f16b7 commit f44a6da
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 66 deletions.
Binary file modified integration/use-optionals-all/test.bin
Binary file not shown.
38 changes: 23 additions & 15 deletions integration/use-optionals-all/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function stateEnumToJSON(object: StateEnum): string {

export interface OptionalsTest {
id?: number;
child?: Child | undefined;
child?: Child;
state?: StateEnum;
long?: number;
truth?: boolean;
Expand Down Expand Up @@ -89,7 +89,7 @@ const baseOptionalsTest: object = {

export const OptionalsTest = {
encode(message: OptionalsTest, writer: Writer = Writer.create()): Writer {
if (!!message.id) {
if (message.id !== undefined && message.id !== 0) {
writer.uint32(8).int32(message.id);
}
if (message.child !== undefined) {
Expand All @@ -98,7 +98,7 @@ export const OptionalsTest = {
if (message.state !== undefined && message.state !== 0) {
writer.uint32(24).int32(message.state);
}
if (!!message.long) {
if (message.long !== undefined && message.long !== 0) {
writer.uint32(32).int64(message.long);
}
if (message.truth === true) {
Expand Down Expand Up @@ -402,7 +402,7 @@ export const OptionalsTest = {
return obj;
},

fromPartial(object: DeepPartial<OptionalsTest>): OptionalsTest {
fromPartial<I extends Exact<DeepPartial<OptionalsTest>, I>>(object: I): OptionalsTest {
const message = { ...baseOptionalsTest } as OptionalsTest;
message.id = object.id ?? 0;
message.child = object.child !== undefined && object.child !== null ? Child.fromPartial(object.child) : undefined;
Expand All @@ -411,13 +411,13 @@ export const OptionalsTest = {
message.truth = object.truth ?? false;
message.description = object.description ?? '';
message.data = object.data ?? new Uint8Array();
message.repId = (object.repId ?? []).map((e) => e);
message.repChild = (object.repChild ?? []).map((e) => Child.fromPartial(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.repId = object.repId?.map((e) => e) || [];
message.repChild = object.repChild?.map((e) => Child.fromPartial(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.optChild =
object.optChild !== undefined && object.optChild !== null ? Child.fromPartial(object.optChild) : undefined;
Expand All @@ -443,10 +443,10 @@ const baseOptionalsTest_TranslationsEntry: object = { key: '', value: '' };

export const OptionalsTest_TranslationsEntry = {
encode(message: OptionalsTest_TranslationsEntry, writer: Writer = Writer.create()): Writer {
if (message.key !== undefined && message.key !== '') {
if (message.key !== '') {
writer.uint32(10).string(message.key);
}
if (message.value !== undefined && message.value !== '') {
if (message.value !== '') {
writer.uint32(18).string(message.value);
}
return writer;
Expand Down Expand Up @@ -487,7 +487,9 @@ export const OptionalsTest_TranslationsEntry = {
return obj;
},

fromPartial(object: DeepPartial<OptionalsTest_TranslationsEntry>): OptionalsTest_TranslationsEntry {
fromPartial<I extends Exact<DeepPartial<OptionalsTest_TranslationsEntry>, I>>(
object: I
): OptionalsTest_TranslationsEntry {
const message = { ...baseOptionalsTest_TranslationsEntry } as OptionalsTest_TranslationsEntry;
message.key = object.key ?? '';
message.value = object.value ?? '';
Expand Down Expand Up @@ -527,7 +529,7 @@ export const Child = {
return obj;
},

fromPartial(_: DeepPartial<Child>): Child {
fromPartial<I extends Exact<DeepPartial<Child>, I>>(_: I): Child {
const message = { ...baseChild } as Child;
return message;
},
Expand Down Expand Up @@ -566,6 +568,7 @@ function base64FromBytes(arr: Uint8Array): string {
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
Expand All @@ -576,6 +579,11 @@ export type DeepPartial<T> = T extends Builtin
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;

function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER');
Expand Down
90 changes: 48 additions & 42 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isLongValueType,
isMapType,
isMessage,
isOptionalProperty,
isPrimitive,
isRepeated,
isScalar,
Expand Down Expand Up @@ -555,24 +556,6 @@ function makeTimestampMethods(options: Options, longs: ReturnType<typeof makeLon
return { toTimestamp, fromTimestamp, fromJsonTimestamp };
}

// 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
// always be present.
function isOptionalProperty(
field: FieldDescriptorProto,
messageOptions: MessageOptions | undefined,
options: Options
): boolean {
const optionalMessages = options.useOptionals === true || options.useOptionals === 'messages' || options.useOptionals === 'all';
const optionalAll = options.useOptionals === 'all';
return (
(optionalMessages && isMessage(field) && !isRepeated(field)) ||
(optionalAll && !messageOptions?.mapEntry) ||
field.proto3Optional
);
}

// Create the interface with properties
function generateInterfaceDeclaration(
ctx: Context,
Expand Down Expand Up @@ -875,6 +858,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP
throw new Error(`Unhandled field ${field}`);
}

const isOptional = isOptionalProperty(field, messageDesc.options, options);
if (isRepeated(field)) {
if (isMapType(ctx, messageDesc, field)) {
const valueType = (typeMap.get(field.typeName)![2] as DescriptorProto).field[1];
Expand All @@ -886,47 +870,69 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP
}
`
: writeSnippet(`{ ${maybeTypeField} key: key as any, value }`);
const optionalAlternative = isOptional ? ' || {}' : ''
chunks.push(code`
Object.entries(message.${fieldName} || {}).forEach(([key, value]) => {
Object.entries(message.${fieldName}${optionalAlternative}).forEach(([key, value]) => {
${entryWriteSnippet}
});
`);
} else if (packedType(field.type) === undefined) {
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
for (const v of message.${fieldName}) {
${writeSnippet('v!')};
}
const listWriteSnippet = code`
for (const v of message.${fieldName}) {
${writeSnippet('v!')};
}
`);
`;
if (isOptional) {
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
${listWriteSnippet}
}
`);
} else {
chunks.push(listWriteSnippet)
}
} else if (isEnum(field) && options.stringEnums) {
// This is a lot like the `else` clause, but we wrap `fooToNumber` around it.
// Ideally we'd reuse `writeSnippet` here, but `writeSnippet` has the `writer.uint32(tag)`
// embedded inside of it, and we want to drop that so that we can encode it packed
// (i.e. just one tag and multiple values).
const tag = ((field.number << 3) | 2) >>> 0;
const toNumber = getEnumMethod(ctx, field.typeName, 'ToNumber');
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${toReaderCall(field)}(${toNumber}(v));
}
writer.ldelim();
const listWriteSnippet = code`
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${toReaderCall(field)}(${toNumber}(v));
}
`);
writer.ldelim();
`;
if (isOptional) {
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
${listWriteSnippet}
}
`);
} else {
chunks.push(listWriteSnippet)
}
} else {
// Ideally we'd reuse `writeSnippet` but it has tagging embedded inside of it.
const tag = ((field.number << 3) | 2) >>> 0;
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${toReaderCall(field)}(v);
}
writer.ldelim();
const listWriteSnippet = code`
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${toReaderCall(field)}(v);
}
`);
writer.ldelim();
`;
if (isOptional) {
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
${listWriteSnippet}
}
`);
} else {
chunks.push(listWriteSnippet)
}
}
} else if (isWithinOneOfThatShouldBeUnion(options, field)) {
let oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
Expand All @@ -950,7 +956,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP
`);
} else if (isScalar(field) || isEnum(field)) {
chunks.push(code`
if (${notDefaultCheck(ctx, field, `message.${fieldName}`)}) {
if (${notDefaultCheck(ctx, field, messageDesc.options, `message.${fieldName}`)}) {
${writeSnippet(`message.${fieldName}`)};
}
`);
Expand Down
39 changes: 30 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
FieldDescriptorProto_Label,
FieldDescriptorProto_Type,
FileDescriptorProto,
MessageOptions,
MethodDescriptorProto,
ServiceDescriptorProto,
} from 'ts-proto-descriptors';
Expand Down Expand Up @@ -231,8 +232,10 @@ export function defaultValue(ctx: Context, field: FieldDescriptorProto): any {
}

/** Creates code that checks that the field is not the default value. Supports scalars and enums. */
export function notDefaultCheck(ctx: Context, field: FieldDescriptorProto, place: string): Code {
export function notDefaultCheck(ctx: Context, field: FieldDescriptorProto, messageOptions: MessageOptions | undefined, place: string): Code {
const { typeMap, options } = ctx;
const isOptional = isOptionalProperty(field, messageOptions, options);
const maybeNotUndefinedAnd = isOptional ? `${place} !== undefined && ` : "";
switch (field.type) {
case FieldDescriptorProto_Type.TYPE_DOUBLE:
case FieldDescriptorProto_Type.TYPE_FLOAT:
Expand All @@ -241,7 +244,7 @@ export function notDefaultCheck(ctx: Context, field: FieldDescriptorProto, place
case FieldDescriptorProto_Type.TYPE_SINT32:
case FieldDescriptorProto_Type.TYPE_FIXED32:
case FieldDescriptorProto_Type.TYPE_SFIXED32:
return code`!!${place}`;
return code`${maybeNotUndefinedAnd} ${place} !== 0`;
case FieldDescriptorProto_Type.TYPE_ENUM:
// proto3 enforces enums starting at 0, however proto2 does not, so we have
// to probe and see if zero is an allowed value. If it's not, pick the first one.
Expand All @@ -251,28 +254,28 @@ export function notDefaultCheck(ctx: Context, field: FieldDescriptorProto, place
const zerothValue = enumProto.value.find((v) => v.number === 0) || enumProto.value[0];
if (options.stringEnums) {
const enumType = messageToTypeName(ctx, field.typeName);
return code`${place} !== undefined && ${place} !== ${enumType}.${zerothValue.name}`;
return code`${maybeNotUndefinedAnd} ${place} !== ${enumType}.${zerothValue.name}`;
} else {
return code`${place} !== undefined && ${place} !== ${zerothValue.number}`;
return code`${maybeNotUndefinedAnd} ${place} !== ${zerothValue.number}`;
}
case FieldDescriptorProto_Type.TYPE_UINT64:
case FieldDescriptorProto_Type.TYPE_FIXED64:
case FieldDescriptorProto_Type.TYPE_INT64:
case FieldDescriptorProto_Type.TYPE_SINT64:
case FieldDescriptorProto_Type.TYPE_SFIXED64:
if (options.forceLong === LongOption.LONG) {
return code`${place} !== undefined && !${place}.isZero()`;
return code`${maybeNotUndefinedAnd} !${place}.isZero()`;
} else if (options.forceLong === LongOption.STRING) {
return code`${place} !== undefined && ${place} !== "0"`;
return code`${maybeNotUndefinedAnd} ${place} !== "0"`;
} else {
return code`!!${place}`;
return code`${maybeNotUndefinedAnd} ${place} !== 0`;
}
case FieldDescriptorProto_Type.TYPE_BOOL:
return code`${place} === true`;
case FieldDescriptorProto_Type.TYPE_STRING:
return code`${place} !== undefined && ${place} !== ""`;
return code`${maybeNotUndefinedAnd} ${place} !== ""`;
case FieldDescriptorProto_Type.TYPE_BYTES:
return code`${place} !== undefined && ${place}.length !== 0`;
return code`${maybeNotUndefinedAnd} ${place}.length !== 0`;
default:
throw new Error('Not implemented for the given type.');
}
Expand Down Expand Up @@ -325,6 +328,24 @@ export function isScalar(field: FieldDescriptorProto): boolean {
return scalarTypes.includes(field.type);
}

// 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
// always be present.
export function isOptionalProperty(
field: FieldDescriptorProto,
messageOptions: MessageOptions | undefined,
options: Options
): boolean {
const optionalMessages = options.useOptionals === true || options.useOptionals === 'messages' || options.useOptionals === 'all';
const optionalAll = options.useOptionals === 'all';
return (
(optionalMessages && isMessage(field) && !isRepeated(field)) ||
(optionalAll && !messageOptions?.mapEntry) ||
field.proto3Optional
);
}

/** This includes all scalars, enums and the [groups type](https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/DescriptorProtos.FieldDescriptorProto.Type.html#TYPE_GROUP) */
export function isPrimitive(field: FieldDescriptorProto): boolean {
return !isMessage(field);
Expand Down

0 comments on commit f44a6da

Please sign in to comment.