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 support for Struct in NestJS #762

Merged
merged 9 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
30 changes: 19 additions & 11 deletions integration/grpc-js/google/protobuf/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,13 @@ export const Struct = {
},

unwrap(message: Struct): { [key: string]: any } {
if (!message.fields) {
return message;
}
const object: { [key: string]: any } = {};
Object.keys(message.fields).forEach((key) => {
object[key] = message.fields[key];
const unwrappedValue = Value.unwrap(message.fields[key]);
object[key] = unwrappedValue !== undefined ? unwrappedValue : message.fields[key];
});
return object;
},
Expand Down Expand Up @@ -380,18 +384,18 @@ export const Value = {
return result;
},

unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
if (message?.stringValue !== undefined) {
unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined {
if (message?.hasOwnProperty("stringValue") && message?.stringValue !== undefined) {
return message.stringValue;
} else if (message?.numberValue !== undefined) {
} else if (message?.hasOwnProperty("numberValue") && message?.numberValue !== undefined) {
return message.numberValue;
} else if (message?.boolValue !== undefined) {
} else if (message?.hasOwnProperty("boolValue") && message?.boolValue !== undefined) {
return message.boolValue;
} else if (message?.structValue !== undefined) {
return message.structValue;
} else if (message?.listValue !== undefined) {
return message.listValue;
} else if (message?.nullValue !== undefined) {
} else if (message?.hasOwnProperty("structValue") && message?.structValue !== undefined) {
return Struct.unwrap(message.structValue as any);
} else if (message?.hasOwnProperty("listValue") && message?.listValue !== undefined) {
return ListValue.unwrap(message.listValue);
} else if (message?.hasOwnProperty("nullValue") && message?.nullValue !== undefined) {
return null;
}
return undefined;
Expand Down Expand Up @@ -461,7 +465,11 @@ export const ListValue = {
},

unwrap(message: ListValue): Array<any> {
return message.values;
if (message?.hasOwnProperty("values") && Array.isArray(message.values)) {
return message.values.map((value: any) => Value.unwrap(value) || value);
} else {
return message as any;
}
},
};

Expand Down
204 changes: 204 additions & 0 deletions integration/nestjs-simple/google/protobuf/struct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/* eslint-disable */
import { wrappers } from "protobufjs";

export const protobufPackage = "google.protobuf";

/**
* `NullValue` is a singleton enumeration to represent the null value for the
* `Value` type union.
*
* The JSON representation for `NullValue` is JSON `null`.
*/
export enum NullValue {
/** NULL_VALUE - Null value. */
NULL_VALUE = 0,
UNRECOGNIZED = -1,
}

/**
* `Struct` represents a structured data value, consisting of fields
* which map to dynamically typed values. In some languages, `Struct`
* might be supported by a native representation. For example, in
* scripting languages like JS a struct is represented as an
* object. The details of that representation are described together
* with the proto support for the language.
*
* The JSON representation for `Struct` is JSON object.
*/
export interface Struct {
/** Unordered map of dynamically typed values. */
fields: { [key: string]: any | undefined };
}

export interface Struct_FieldsEntry {
key: string;
value: any | undefined;
}

/**
* `Value` represents a dynamically typed value which can be either
* null, a number, a string, a boolean, a recursive struct value, or a
* list of values. A producer of value is expected to set one of these
* variants. Absence of any variant indicates an error.
*
* The JSON representation for `Value` is JSON value.
*/
export interface Value {
/** Represents a null value. */
nullValue?:
| NullValue
| undefined;
/** Represents a double value. */
numberValue?:
| number
| undefined;
/** Represents a string value. */
stringValue?:
| string
| undefined;
/** Represents a boolean value. */
boolValue?:
| boolean
| undefined;
/** Represents a structured value. */
structValue?:
| { [key: string]: any }
| undefined;
/** Represents a repeated `Value`. */
listValue?: Array<any> | undefined;
}

/**
* `ListValue` is a wrapper around a repeated field of values.
*
* The JSON representation for `ListValue` is JSON array.
*/
export interface ListValue {
/** Repeated field of dynamically typed values. */
values: any[];
}

export const GOOGLE_PROTOBUF_PACKAGE_NAME = "google.protobuf";

const wrapStruct = (value: any, nested = false): any => {
const valueType = typeof value;
const primitiveValueTypes = { number: "numberValue", string: "stringValue", boolean: "boolValue" };
if (Object.keys(primitiveValueTypes).includes(valueType)) {
return Value.wrap(value);
}
if (Array.isArray(value)) {
return { listValue: { values: value.map((item) => wrapStruct(item)) } };
}
if (valueType === "object") {
const res = nested ? { structValue: { fields: {} as any } } : { fields: {} as any };
Object.keys(value).forEach((field) => {
if (nested) {
res.structValue!.fields[field] = wrapStruct(value[field], true);
} else {
res.fields![field] = wrapStruct(value[field], true);
}
});
return res;
}
};
wrappers[".google.protobuf.Struct"] = {
fromObject: wrapStruct,
toObject(message: Struct) {
return message ? Struct.unwrap(message) : message;
},
} as any;

function createBaseStruct(): Struct {
return { fields: {} };
}

export const Struct = {
wrap(object: { [key: string]: any } | undefined): Struct {
const struct = createBaseStruct();
if (object !== undefined) {
Object.keys(object).forEach((key) => {
struct.fields[key] = object[key];
});
}
return struct;
},

unwrap(message: Struct): { [key: string]: any } {
if (!message.fields) {
return message;
}
const object: { [key: string]: any } = {};
Object.keys(message.fields).forEach((key) => {
const unwrappedValue = Value.unwrap(message.fields[key]);
object[key] = unwrappedValue !== undefined ? unwrappedValue : message.fields[key];
});
return object;
},
};

function createBaseValue(): Value {
return {};
}

export const Value = {
wrap(value: any): Value {
const result = createBaseValue();

if (value === null) {
result.nullValue = NullValue.NULL_VALUE;
} else if (typeof value === "boolean") {
result.boolValue = value;
} else if (typeof value === "number") {
result.numberValue = value;
} else if (typeof value === "string") {
result.stringValue = value;
} else if (Array.isArray(value)) {
result.listValue = value;
} else if (typeof value === "object") {
result.structValue = value;
} else if (typeof value !== "undefined") {
throw new Error("Unsupported any value type: " + typeof value);
}

return result;
},

unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined {
if (message?.hasOwnProperty("stringValue") && message?.stringValue !== undefined) {
return message.stringValue;
} else if (message?.hasOwnProperty("numberValue") && message?.numberValue !== undefined) {
return message.numberValue;
} else if (message?.hasOwnProperty("boolValue") && message?.boolValue !== undefined) {
return message.boolValue;
} else if (message?.hasOwnProperty("structValue") && message?.structValue !== undefined) {
return Struct.unwrap(message.structValue as any);
} else if (message?.hasOwnProperty("listValue") && message?.listValue !== undefined) {
return ListValue.unwrap(message.listValue);
} else if (message?.hasOwnProperty("nullValue") && message?.nullValue !== undefined) {
return null;
}
return undefined;
},
};

function createBaseListValue(): ListValue {
return { values: [] };
}

export const ListValue = {
wrap(value: Array<any> | undefined): ListValue {
const result = createBaseListValue();

result.values = value ?? [];

return result;
},

unwrap(message: ListValue): Array<any> {
if (message?.hasOwnProperty("values") && Array.isArray(message.values)) {
return message.values.map((value: any) => Value.unwrap(value) || value);
} else {
return message as any;
}
},
};
Binary file modified integration/nestjs-simple/hero.bin
Binary file not shown.
4 changes: 3 additions & 1 deletion integration/nestjs-simple/hero.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ syntax = "proto3";

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";

package hero;

Expand All @@ -25,7 +26,8 @@ message VillainById {
message Hero {
int32 id = 1;
string name = 2;
google.protobuf.Timestamp birth_date = 3;
.google.protobuf.Timestamp birth_date = 3;
.google.protobuf.Struct external_data = 4;
}

message Villain {
Expand Down
31 changes: 31 additions & 0 deletions integration/nestjs-simple/hero.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable */
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { wrappers } from "protobufjs";
import { Observable } from "rxjs";
import { Empty } from "./google/protobuf/empty";
import { Struct, Value } from "./google/protobuf/struct";
import { Timestamp } from "./google/protobuf/timestamp";

export const protobufPackage = "hero";
Expand All @@ -18,6 +20,7 @@ export interface Hero {
id: number;
name: string;
birthDate: Timestamp | undefined;
externalData: { [key: string]: any } | undefined;
}

export interface Villain {
Expand All @@ -27,6 +30,34 @@ export interface Villain {

export const HERO_PACKAGE_NAME = "hero";

const wrapStruct = (value: any, nested = false): any => {
const valueType = typeof value;
const primitiveValueTypes = { number: "numberValue", string: "stringValue", boolean: "boolValue" };
if (Object.keys(primitiveValueTypes).includes(valueType)) {
return Value.wrap(value);
}
if (Array.isArray(value)) {
return { listValue: { values: value.map((item) => wrapStruct(item)) } };
}
if (valueType === "object") {
const res = nested ? { structValue: { fields: {} as any } } : { fields: {} as any };
Object.keys(value).forEach((field) => {
if (nested) {
res.structValue!.fields[field] = wrapStruct(value[field], true);
} else {
res.fields![field] = wrapStruct(value[field], true);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should this true be a false? Just thinking that we are in the nested = false branch of the it, so should we pass along false as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only case we actually need for nested to be false is the base object, we should only get here initially and from here it should be always true

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, thanks!

}
});
return res;
}
};
wrappers[".google.protobuf.Struct"] = {
fromObject: wrapStruct,
toObject(message: Struct) {
return message ? Struct.unwrap(message) : message;
},
} as any;

export interface HeroServiceClient {
addOneHero(request: Hero): Observable<Empty>;

Expand Down
4 changes: 2 additions & 2 deletions integration/nestjs-simple/nestjs-project/hero.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Hero, HeroById, HeroServiceController, HeroServiceControllerMethods, Vi
@HeroServiceControllerMethods()
export class HeroController implements HeroServiceController {
private readonly heroes: Hero[] = [
{ id: 1, name: 'Stephenh', birthDate: { seconds: 1, nanos: 2 } },
{ id: 2, name: 'Iangregsondev', birthDate: { seconds: 1, nanos: 3 } },
{ id: 1, name: 'Stephenh', birthDate: { seconds: 1, nanos: 2 }, externalData: { foo: 'bar', fizz: 1, nested: { isFailing: false, arr: [1,'foo',['bar']] } } },
{ id: 2, name: 'Iangregsondev', birthDate: { seconds: 1, nanos: 3 }, externalData: { bar: 10, baz: 'foo', isPassing: true } },
];

private readonly villains: Villain[] = [{ id: 1, name: 'John' }, { id: 2, name: 'Doe' }];
Expand Down
Loading