Skip to content

Commit 022be28

Browse files
tatethurstonTate
and
Tate
authored
add map type (#75)
Co-authored-by: Tate <[email protected]>
1 parent 1db68fa commit 022be28

File tree

3 files changed

+139
-64
lines changed

3 files changed

+139
-64
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"build:commonjs": "yarn tsc",
2222
"build:module": "yarn tsc --module esnext --outDir dist/module",
2323
"build:watch": "yarn build:commonjs --watch",
24-
"check:all": "yarn examples:check && yarn test && yarn test:compat && yarn test:e2e:setup",
24+
"check:all": "yarn examples:check && yarn test && yarn test:compat && yarn test:e2e",
2525
"clean": "rm -rf dist",
2626
"examples:check": "for example in examples/*; do (cd \"$example\" && yarn twirpscript && yarn tsc --noEmit) || exit 1; done",
2727
"examples:setup": "yarn build && npm link && for example in examples/*; do (cd \"$example\" && npm link twirpscript && yarn); done",

Diff for: src/autogenerate/index.ts

+110-58
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ProtoTypes,
66
Service,
77
processTypes,
8+
MessageType,
89
} from "../utils";
910
import { RUNTIME_MIN_CODE_GEN_SUPPORTED_VERSION } from "../runtimeCodegenCompat";
1011

@@ -20,6 +21,11 @@ function writeTypes(types: ProtoTypes[]): string {
2021
}
2122
if (node.type === "enum") {
2223
result += `export type ${name} = typeof ${node.content.fullyQualifiedName}[keyof typeof ${node.content.fullyQualifiedName}];\n\n`;
24+
} else if (node.type === "message" && node.content.isMap) {
25+
result += `export type ${name} = Record<
26+
${node.content.fields[0].tsType},
27+
${node.content.fields[1].tsType} | undefined>;`;
28+
result += `\n\n`;
2329
} else {
2430
result += `export interface ${name} {\n`;
2531
node.content.fields.forEach(
@@ -62,26 +68,39 @@ function writeSerializers(types: ProtoTypes[], isTopLevel = true): string {
6268
`: void`
6369
)} {
6470
${node.content.fields
65-
.map(
66-
(field) => `\
67-
${
68-
field.repeated
69-
? `if (msg.${field.name}.length > 0) {`
70-
: field.optional
71-
? `if (msg.${field.name} != undefined) {`
72-
: `if (msg.${field.name}) {`
73-
}
74-
${
75-
field.read === "readMessage"
76-
? `writer.${field.write}(${field.index}, msg.${
77-
field.name
78-
} ${field.repeated ? printIfTypescript("as any") : ""}, ${
79-
field.tsType
80-
}.writeMessage);`
81-
: `writer.${field.write}(${field.index}, msg.${field.name});`
82-
}
83-
}`
84-
)
71+
.map((field) => {
72+
let res = "";
73+
if (field.repeated) {
74+
res += `if (msg.${field.name}.length > 0) {`;
75+
} else if (field.optional) {
76+
res += `if (msg.${field.name} != undefined) {`;
77+
} else {
78+
res += `if (msg.${field.name}) {`;
79+
}
80+
81+
if (field.read === "readMessage") {
82+
res += `writer.${field.write}(${field.index}, msg.${
83+
field.name
84+
} ${field.repeated ? printIfTypescript("as any") : ""}, ${
85+
field.tsType
86+
}.writeMessage);`;
87+
} else if (field.read === "map") {
88+
const map = node.children.find(
89+
(c) => c.content.fullyQualifiedName === field.tsType
90+
) as MessageType;
91+
res += `for (const key in msg.${field.name}) {
92+
writer.writeMessage(${field.index}, {}, (_, mapWriter) => {
93+
mapWriter.${map.content.fields[0].write}(1, key as any);
94+
mapWriter.${map.content.fields[1].write}(2, msg.foo[key]);
95+
});
96+
}`;
97+
} else {
98+
res += `writer.${field.write}(${field.index}, msg.${field.name});`;
99+
}
100+
101+
res += "}";
102+
return res;
103+
})
85104
.join("\n")}
86105
},
87106
@@ -101,47 +120,76 @@ function writeSerializers(types: ProtoTypes[], isTopLevel = true): string {
101120
": void "
102121
)}{
103122
${node.content.fields
104-
.filter(({ repeated }) => repeated)
105-
.map((field) => `msg.${field.name} = [];`)
123+
.map((field) => {
124+
if (field.repeated) {
125+
return `msg.${field.name} = [];`;
126+
} else if (field.read === "map") {
127+
return `msg.${field.name} = {};`;
128+
}
129+
})
130+
.filter(Boolean)
106131
.join("\n")}
107132
while (reader.nextField()) {
108133
const field = reader.getFieldNumber();
109134
switch (field) {
110135
${node.content.fields
111-
.map(
112-
(field) => `\
113-
case ${field.index}: {
114-
${
115-
field.read === "readMessage"
116-
? `\
117-
const message = {};
118-
reader.readMessage(message, ${field.tsType}.readMessage);
119-
${
120-
field.repeated
121-
? `msg.${field.name}.push(message${printIfTypescript(
122-
` as ${field.tsType}`
123-
)});`
124-
: `msg.${field.name} = message ${printIfTypescript(
125-
`as ${field.tsType}`
126-
)};`
127-
}`
128-
: `${
129-
field.repeated
130-
? `msg.${field.name}.push(reader.${field.read}() ${
131-
field.read === "readEnum"
132-
? printIfTypescript(`as ${field.tsType}`)
133-
: ""
134-
});`
135-
: `msg.${field.name} = reader.${field.read}() ${
136-
field.read === "readEnum"
137-
? printIfTypescript(`as ${field.tsType}`)
138-
: ""
139-
};`
140-
}`
141-
}
142-
break;
143-
}`
144-
)
136+
.map((field) => {
137+
let res = "";
138+
res += `case ${field.index}: {`;
139+
if (field.read == "map") {
140+
const map = node.children.find(
141+
(c) => c.content.fullyQualifiedName === field.tsType
142+
) as MessageType;
143+
res += `reader.readMessage(undefined, () => {
144+
let key: ${map.content.fields[0].tsType};
145+
let value: ${map.content.fields[1].tsType};
146+
while (reader.nextField()) {
147+
const field = reader.getFieldNumber();
148+
switch (field) {
149+
case 1: {
150+
key = reader.${map.content.fields[0].read}();
151+
break;
152+
}
153+
case 2: {
154+
value = reader.${map.content.fields[1].read}();
155+
break;
156+
}
157+
}
158+
}
159+
msg.${field.name}![key!] = value!;
160+
});`;
161+
} else if (field.read === "readMessage") {
162+
res += `
163+
const message = {};
164+
reader.readMessage(message, ${field.tsType}.readMessage);
165+
`;
166+
if (field.repeated) {
167+
res += `msg.${field.name}.push(message${printIfTypescript(
168+
` as ${field.tsType}`
169+
)});`;
170+
} else {
171+
res += `msg.${field.name} = message ${printIfTypescript(
172+
`as ${field.tsType}`
173+
)};`;
174+
}
175+
} else {
176+
if (field.repeated) {
177+
res += `msg.${field.name}.push(reader.${field.read}() ${
178+
field.read === "readEnum"
179+
? printIfTypescript(`as ${field.tsType}`)
180+
: ""
181+
});`;
182+
} else {
183+
res += `msg.${field.name} = reader.${field.read}() ${
184+
field.read === "readEnum"
185+
? printIfTypescript(`as ${field.tsType}`)
186+
: ""
187+
};`;
188+
}
189+
}
190+
res += "break;\n}";
191+
return res;
192+
})
145193
.join("\n")}
146194
default: {
147195
reader.skipField();
@@ -198,8 +246,12 @@ function writeSerializers(types: ProtoTypes[], isTopLevel = true): string {
198246
},
199247
200248
`;
201-
if (node.children.length > 0) {
202-
result += writeSerializers(node.children, false);
249+
const childrenWithouMaps = node.children.filter(
250+
(x) => x.type !== "message" || !x.content.isMap
251+
);
252+
253+
if (childrenWithouMaps.length > 0) {
254+
result += writeSerializers(childrenWithouMaps, false);
203255
}
204256
result += `}${isTopLevel ? ";" : ","}\n\n`;
205257
break;

Diff for: src/utils.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export function commandIsInPath(cmd: string): boolean {
4646
}
4747
}
4848

49-
type ReaderMethod = keyof BinaryReader;
50-
type WriterMethod = keyof BinaryWriter;
49+
type ReaderMethod = keyof BinaryReader | "map";
50+
type WriterMethod = keyof BinaryWriter | "map";
5151

5252
interface Descriptor {
5353
defaultValue: string;
@@ -183,6 +183,21 @@ export function getDescriptor(
183183
identifierTable,
184184
fileDescriptorProto
185185
);
186+
// Hack until better option:
187+
// https://github.com/protocolbuffers/protobuf/issues/9369
188+
const isMap =
189+
_type.endsWith("Entry") && !field.toArray()[0].endsWith("Entry");
190+
191+
if (isMap) {
192+
return {
193+
defaultValue: "{}",
194+
optional,
195+
read: "map",
196+
repeated: false,
197+
tsType: name.slice(0, name.lastIndexOf("Entry")),
198+
write: "map",
199+
};
200+
}
186201

187202
return {
188203
defaultValue: "undefined",
@@ -445,10 +460,11 @@ interface MessageOpts {
445460
fullyQualifiedName: string;
446461
fields: Field[];
447462
comments?: Comments;
463+
isMap: boolean;
448464
}
449465

450-
type EnumType = { type: "enum"; content: EnumOpts };
451-
type MessageType = {
466+
export type EnumType = { type: "enum"; content: EnumOpts };
467+
export type MessageType = {
452468
type: "message";
453469
content: MessageOpts;
454470
children: ProtoTypes[];
@@ -627,15 +643,22 @@ export function processTypes(
627643
}
628644

629645
function getMessage(namespacing: string, node: DescriptorProto): MessageOpts {
630-
const name = node.getName();
646+
let name = node.getName();
631647
if (!name) {
632648
throw new Error(`Expected name for ${node}`);
633649
}
650+
651+
// Hack until better option:
652+
// https://github.com/protocolbuffers/protobuf/issues/9369
653+
const isMap = name.endsWith("Entry") && node.getFieldList().length == 2;
654+
name = isMap ? name.slice(0, name.lastIndexOf("Entry")) : name;
655+
634656
const opts: MessageOpts = {
635657
name,
636658
fullyQualifiedName: applyNamespace(namespacing, name, {
637659
removeLeadingPeriod: true,
638660
}),
661+
isMap,
639662
fields: node
640663
.getFieldList()
641664
.map((value) => {

0 commit comments

Comments
 (0)