Skip to content

Commit e12319f

Browse files
feat: allow strings for enums, bytes, and Long in .d.ts (#714)
1 parent 630b085 commit e12319f

5 files changed

Lines changed: 169 additions & 17 deletions

File tree

packages/packages/google-gax/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ npm-debug.log
55
.coverage
66
.nyc_output
77
docs/
8-
google/
8+
protos/google/
99
out/
1010
system-test/secrets.js
1111
system-test/*key.json
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
package google;
18+
19+
service DtsUpdateTest {
20+
rpc Test(TestMessage) returns (TestMessage) {
21+
}
22+
}
23+
24+
enum TestEnum {
25+
TEST_ENUM_UNSPECIFIED = 0;
26+
SOME_VALUE = 1;
27+
}
28+
29+
message TestMessage {
30+
int64 long_field = 1;
31+
bytes bytes_field = 2;
32+
TestEnum enum_field = 3;
33+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["./google/dts.proto"]

packages/packages/google-gax/test/unit/compileProtos.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ const testDir = path.join(process.cwd(), '.compileProtos-test');
3131
const resultDir = path.join(testDir, 'protos');
3232
const cwd = process.cwd();
3333

34+
const expectedJsonResultFile = path.join(resultDir, 'protos.json');
35+
const expectedJSResultFile = path.join(resultDir, 'protos.js');
36+
const expectedTSResultFile = path.join(resultDir, 'protos.d.ts');
37+
3438
describe('compileProtos tool', () => {
35-
before(async () => {
39+
beforeEach(async () => {
3640
if (fs.existsSync(testDir)) {
3741
await rmrf(testDir);
3842
}
@@ -42,17 +46,14 @@ describe('compileProtos tool', () => {
4246
process.chdir(testDir);
4347
});
4448

45-
after(() => {
49+
afterEach(() => {
4650
process.chdir(cwd);
4751
});
4852

4953
it('compiles protos to JSON, JS, TS', async () => {
5054
await compileProtos.main([
5155
path.join(__dirname, '..', '..', 'test', 'fixtures', 'protoLists'),
5256
]);
53-
const expectedJsonResultFile = path.join(resultDir, 'protos.json');
54-
const expectedJSResultFile = path.join(resultDir, 'protos.js');
55-
const expectedTSResultFile = path.join(resultDir, 'protos.d.ts');
5657
assert(fs.existsSync(expectedJsonResultFile));
5758
assert(fs.existsSync(expectedJSResultFile));
5859
assert(fs.existsSync(expectedTSResultFile));
@@ -90,10 +91,40 @@ describe('compileProtos tool', () => {
9091
'empty'
9192
),
9293
]);
93-
const expectedResultFile = path.join(resultDir, 'protos.json');
94-
assert(fs.existsSync(expectedResultFile));
94+
assert(fs.existsSync(expectedJsonResultFile));
9595

96-
const json = await readFile(expectedResultFile);
96+
const json = await readFile(expectedJsonResultFile);
9797
assert.strictEqual(json.toString(), '{}');
9898
});
99+
100+
it('fixes types in the TS file', async () => {
101+
await compileProtos.main([
102+
path.join(__dirname, '..', '..', 'test', 'fixtures', 'dts-update'),
103+
]);
104+
assert(fs.existsSync(expectedTSResultFile));
105+
const ts = await readFile(expectedTSResultFile);
106+
107+
assert(ts.toString().includes('import * as Long'));
108+
assert(
109+
ts.toString().includes('http://www.apache.org/licenses/LICENSE-2.0')
110+
);
111+
assert(ts.toString().includes('longField?: (number|Long|string|null);'));
112+
assert(ts.toString().includes('bytesField?: (Uint8Array|string|null);'));
113+
assert(
114+
ts
115+
.toString()
116+
.includes(
117+
'enumField?: (google.TestEnum|keyof typeof google.TestEnum|null);'
118+
)
119+
);
120+
assert(ts.toString().includes('public longField: (number|Long|string);'));
121+
assert(ts.toString().includes('public bytesField: (Uint8Array|string);'));
122+
assert(
123+
ts
124+
.toString()
125+
.includes(
126+
'public enumField: (google.TestEnum|keyof typeof google.TestEnum);'
127+
)
128+
);
129+
});
99130
});

packages/packages/google-gax/tools/compileProtos.ts

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,100 @@ function normalizePath(filePath: string): string {
8080
return path.join(...filePath.split('/'));
8181
}
8282

83+
function getAllEnums(dts: string): Set<string> {
84+
const result = new Set<string>();
85+
const lines = dts.split('\n');
86+
const nestedIds = [];
87+
let currentEnum = undefined;
88+
for (const line of lines) {
89+
const match = line.match(
90+
/^\s*(?:export )?(namespace|class|interface|enum) (\w+) .*{/
91+
);
92+
if (match) {
93+
const [, keyword, id] = match;
94+
nestedIds.push(id);
95+
if (keyword === 'enum') {
96+
currentEnum = nestedIds.join('.');
97+
result.add(currentEnum);
98+
}
99+
continue;
100+
}
101+
if (line.match(/^\s*}/)) {
102+
nestedIds.pop();
103+
currentEnum = undefined;
104+
continue;
105+
}
106+
}
107+
108+
return result;
109+
}
110+
111+
function updateDtsTypes(dts: string, enums: Set<string>): string {
112+
const lines = dts.split('\n');
113+
const result: string[] = [];
114+
115+
for (const line of lines) {
116+
let typeName: string | undefined = undefined;
117+
// Enums can be used in interfaces and in classes.
118+
// For simplicity, we'll check these two cases independently.
119+
// encoding?: (google.cloud.speech.v1p1beta1.RecognitionConfig.AudioEncoding|null);
120+
const interfaceMatch = line.match(/\w+\?: \(([\w.]+)\|null\);/);
121+
if (interfaceMatch) {
122+
typeName = interfaceMatch[1];
123+
}
124+
// public encoding: google.cloud.speech.v1p1beta1.RecognitionConfig.AudioEncoding;
125+
const classMatch = line.match(/public \w+: ([\w.]+);/);
126+
if (classMatch) {
127+
typeName = classMatch[1];
128+
}
129+
130+
if (line.match(/\(number\|Long(?:\|null)?\)/)) {
131+
typeName = 'Long';
132+
}
133+
134+
let replaced = line;
135+
if (typeName && enums.has(typeName)) {
136+
// enum: E => E|keyof typeof E to allow all string values
137+
replaced = replaced.replace(
138+
typeName,
139+
`${typeName}|keyof typeof ${typeName}`
140+
);
141+
} else if (typeName === 'Uint8Array') {
142+
// bytes: Uint8Array => Uint8Array|string to allow base64-encoded strings
143+
replaced = replaced.replace(typeName, `${typeName}|string`);
144+
} else if (typeName === 'Long') {
145+
// Longs can be passed as strings :(
146+
// number|Long => number|Long|string
147+
replaced = replaced.replace('number|Long', 'number|Long|string');
148+
}
149+
150+
// add brackets if we have added a |
151+
replaced = replaced.replace(/: ([\w.]+\|[ \w.|]+);/, ': ($1);');
152+
153+
result.push(replaced);
154+
}
155+
156+
return result.join('\n');
157+
}
158+
159+
function fixDtsFile(dts: string): string {
160+
// 1. fix for pbts output: the corresponding protobufjs PR
161+
// https://github.com/protobufjs/protobuf.js/pull/1166
162+
// is merged but not yet released.
163+
if (!dts.match(/import \* as Long/)) {
164+
dts = 'import * as Long from "long";\n' + dts;
165+
}
166+
167+
// 2. add Apache license to the generated .d.ts file
168+
dts = apacheLicense + dts;
169+
170+
// 3. major hack: update types to allow passing strings
171+
// where enums, longs, or bytes are expected
172+
const enums = getAllEnums(dts);
173+
dts = updateDtsTypes(dts, enums);
174+
return dts;
175+
}
176+
83177
/**
84178
* Returns a combined list of proto files listed in all JSON files given.
85179
*
@@ -153,14 +247,7 @@ async function compileProtos(protos: string[]): Promise<void> {
153247
await pbtsMain(pbjsArgs4ts);
154248

155249
let tsResult = (await readFile(tsOutput)).toString();
156-
// fix for pbts output: the corresponding protobufjs PR
157-
// https://github.com/protobufjs/protobuf.js/pull/1166
158-
// is merged but not yet released.
159-
if (!tsResult.match(/import \* as Long/)) {
160-
tsResult = 'import * as Long from "long";\n' + tsResult;
161-
}
162-
// add Apache license to the generated .d.ts file
163-
tsResult = apacheLicense + tsResult;
250+
tsResult = fixDtsFile(tsResult);
164251
await writeFile(tsOutput, tsResult);
165252
}
166253

0 commit comments

Comments
 (0)