Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option noDefaultsForOptionals #1051

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,11 @@ export interface User {
}
```

- With `--ts_proto_opt=noDefaultsForOptionals=true`, `undefined` primitive values will not be defaulted as per the protobuf spec. Additionally unlike the standard behavior, when a field is set to it's standard default value, it *will* be encoded allowing it to be sent over the wire and distinguished from undefined values. For example if a message does not set a boolean value, ordinarily this would be defaulted to `false` which is different to it being undefined.

This option allows the library to act in a compatible way with the [Wire implementation](https://square.github.io/wire/) maintained and used by Square/Block. Note: this option should only be used in combination with other client/server code generated using Wire or ts-proto with this option enabled.


### NestJS Support

We have a great way of working together with [nestjs](https://docs.nestjs.com/microservices/grpc). `ts-proto` generates `interfaces` and `decorators` for you controller, client. For more information see the [nestjs readme](NESTJS.markdown).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Proto2TestMessage } from './proto-2';

describe('proto2', () => {
it('preserves null values on optional fields', () => {
const message = Proto2TestMessage.fromJSON({
intValue: null,
stringValue: null,
boolValue: null,
mapValue: {},
});

const data = Proto2TestMessage.encode(message).finish();
const result = Proto2TestMessage.decode(data);

expect(result).toEqual(message);
});

it('encodes/decodes non-null values on optional fields', () => {
const message = Proto2TestMessage.fromJSON({
intValue: 100,
stringValue: 'string',
boolValue: true,
mapValue: {
v1:'1',
v2:'2',
}
});

const data = Proto2TestMessage.encode(message).finish();
const result = Proto2TestMessage.decode(data);

expect(result).toEqual(message);
});

it('encodes/decodes non-null values set to standard protobuf defaults', () => {
const message = Proto2TestMessage.fromJSON({
intValue: 0,
stringValue: '',
boolValue: false,
mapValue: {},
});

const data = Proto2TestMessage.encode(message).finish();
const result = Proto2TestMessage.decode(data);

expect(result).toEqual(message);
});
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
noDefaultsForOptionals=true,useNullAsOptional=true
Binary file not shown.
11 changes: 11 additions & 0 deletions integration/no-defaults-for-optionals-with-nulls/proto-2.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto2";

package omit;

message Proto2TestMessage {
optional bool boolValue = 1;
optional int32 intValue = 2;
optional string stringValue = 3;

map<string, string> mapValue = 4;
}
238 changes: 238 additions & 0 deletions integration/no-defaults-for-optionals-with-nulls/proto-2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// source: proto-2.proto

/* eslint-disable */
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "omit";

export interface Proto2TestMessage {
boolValue?: boolean | null;
intValue?: number | null;
stringValue?: string | null;
mapValue: { [key: string]: string };
}

export interface Proto2TestMessage_MapValueEntry {
key?: string | null;
value?: string | null;
}

function createBaseProto2TestMessage(): Proto2TestMessage {
return { boolValue: null, intValue: null, stringValue: null, mapValue: {} };
}

export const Proto2TestMessage = {
encode(message: Proto2TestMessage, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.boolValue !== undefined && message.boolValue !== null) {
writer.uint32(8).bool(message.boolValue);
}
if (message.intValue !== undefined && message.intValue !== null) {
writer.uint32(16).int32(message.intValue);
}
if (message.stringValue !== undefined && message.stringValue !== null) {
writer.uint32(26).string(message.stringValue);
}
Object.entries(message.mapValue).forEach(([key, value]) => {
Proto2TestMessage_MapValueEntry.encode({ key: key as any, value }, writer.uint32(34).fork()).ldelim();
});
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): Proto2TestMessage {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseProto2TestMessage();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}

message.boolValue = reader.bool();
continue;
case 2:
if (tag !== 16) {
break;
}

message.intValue = reader.int32();
continue;
case 3:
if (tag !== 26) {
break;
}

message.stringValue = reader.string();
continue;
case 4:
if (tag !== 34) {
break;
}

const entry4 = Proto2TestMessage_MapValueEntry.decode(reader, reader.uint32());
if (entry4.value !== undefined && entry4.value !== null && entry4.key !== undefined && entry4.key !== null) {
message.mapValue[entry4.key] = entry4.value;
}
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},

fromJSON(object: any): Proto2TestMessage {
return {
boolValue: isSet(object.boolValue) ? globalThis.Boolean(object.boolValue) : null,
intValue: isSet(object.intValue) ? globalThis.Number(object.intValue) : null,
stringValue: isSet(object.stringValue) ? globalThis.String(object.stringValue) : null,
mapValue: isObject(object.mapValue)
? Object.entries(object.mapValue).reduce<{ [key: string]: string }>((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {})
: {},
};
},

toJSON(message: Proto2TestMessage): unknown {
const obj: any = {};
if (message.boolValue !== undefined && message.boolValue !== null) {
obj.boolValue = message.boolValue;
}
if (message.intValue !== undefined && message.intValue !== null) {
obj.intValue = Math.round(message.intValue);
}
if (message.stringValue !== undefined && message.stringValue !== null) {
obj.stringValue = message.stringValue;
}
if (message.mapValue) {
const entries = Object.entries(message.mapValue);
if (entries.length > 0) {
obj.mapValue = {};
entries.forEach(([k, v]) => {
obj.mapValue[k] = v;
});
}
}
return obj;
},

create<I extends Exact<DeepPartial<Proto2TestMessage>, I>>(base?: I): Proto2TestMessage {
return Proto2TestMessage.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Proto2TestMessage>, I>>(object: I): Proto2TestMessage {
const message = createBaseProto2TestMessage();
message.boolValue = object.boolValue ?? null;
message.intValue = object.intValue ?? null;
message.stringValue = object.stringValue ?? null;
message.mapValue = Object.entries(object.mapValue ?? {}).reduce<{ [key: string]: string }>((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = globalThis.String(value);
}
return acc;
}, {});
return message;
},
};

function createBaseProto2TestMessage_MapValueEntry(): Proto2TestMessage_MapValueEntry {
return { key: null, value: null };
}

export const Proto2TestMessage_MapValueEntry = {
encode(message: Proto2TestMessage_MapValueEntry, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.key !== undefined && message.key !== null) {
writer.uint32(10).string(message.key);
}
if (message.value !== undefined && message.value !== null) {
writer.uint32(18).string(message.value);
}
return writer;
},

decode(input: _m0.Reader | Uint8Array, length?: number): Proto2TestMessage_MapValueEntry {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseProto2TestMessage_MapValueEntry();
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): Proto2TestMessage_MapValueEntry {
return {
key: isSet(object.key) ? globalThis.String(object.key) : null,
value: isSet(object.value) ? globalThis.String(object.value) : null,
};
},

toJSON(message: Proto2TestMessage_MapValueEntry): unknown {
const obj: any = {};
if (message.key !== undefined && message.key !== null) {
obj.key = message.key;
}
if (message.value !== undefined && message.value !== null) {
obj.value = message.value;
}
return obj;
},

create<I extends Exact<DeepPartial<Proto2TestMessage_MapValueEntry>, I>>(base?: I): Proto2TestMessage_MapValueEntry {
return Proto2TestMessage_MapValueEntry.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Proto2TestMessage_MapValueEntry>, I>>(
object: I,
): Proto2TestMessage_MapValueEntry {
const message = createBaseProto2TestMessage_MapValueEntry();
message.key = object.key ?? null;
message.value = object.value ?? null;
return message;
},
};

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

export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [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]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isObject(value: any): boolean {
return typeof value === "object" && value !== null;
}

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
Binary file not shown.
15 changes: 15 additions & 0 deletions integration/no-defaults-for-optionals-with-nulls/proto-3.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
syntax = "proto3";

package omit;

message Proto3TestMessage {
bool boolValue = 1;
int32 intValue = 2;
string stringValue = 3;

optional bool optionalBoolValue = 4;
optional int32 optionalIntValue = 5;
optional string optionalStringValue = 6;

map<string, string> mapValue = 7;
}
Loading
Loading