fix(grpc-reflection): handle references to root-level message types in default package

This commit is contained in:
Justin Timmons 2024-02-21 22:19:37 -05:00
parent 0ba7d70fb9
commit 7c0511f2df
6 changed files with 41 additions and 6 deletions

View File

@ -3,6 +3,7 @@ syntax = "proto3";
package sample;
import 'vendor.proto';
import 'unscoped.proto';
service SampleService {
rpc Hello (HelloRequest) returns (HelloResponse) {}

View File

@ -0,0 +1,10 @@
syntax = "proto3";
message TopLevelMessage {
bool value = 1;
}
message ProcessRequest {
string key = 1;
TopLevelMessage message = 2;
}

View File

@ -3,9 +3,9 @@
* @example scope('grpc.reflection.v1.Type') == 'grpc.reflection.v1'
*/
export const scope = (path: string, separator: string = '.') => {
if (!path.includes(separator)) {
if (!path.includes(separator) || path === separator) {
return '';
}
return path.split(separator).slice(0, -1).join(separator);
return path.split(separator).slice(0, -1).join(separator) || separator;
};

View File

@ -114,12 +114,12 @@ export class ReflectionV1Implementation {
let referencedFile: IFileDescriptorProto | null = null;
if (ref.startsWith('.')) {
// absolute reference -- just remove the leading '.' and use the ref directly
referencedFile = this.symbols[ref.replace(/^\./, '')];
referencedFile = this.symbols[ref.slice(1)];
} else {
// relative reference -- need to seek upwards up the current package scope until we find it
let pkg = pkgScope;
while (pkg && !referencedFile) {
referencedFile = this.symbols[`${pkg}.${ref}`];
referencedFile = this.symbols[`${pkg.replace(/\.$/, '')}.${ref}`];
pkg = scope(pkg);
}

View File

@ -10,7 +10,10 @@ describe('GrpcReflectionService', () => {
beforeEach(async () => {
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
includeDirs: [
path.join(__dirname, '../proto/sample/'),
path.join(__dirname, '../proto/sample/vendor')
]
});
reflectionService = new ReflectionV1Implementation(root);
@ -25,7 +28,10 @@ describe('GrpcReflectionService', () => {
it('whitelists services properly', () => {
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
includeDirs: [
path.join(__dirname, '../proto/sample/'),
path.join(__dirname, '../proto/sample/vendor')
]
});
reflectionService = new ReflectionV1Implementation(root, { services: ['sample.SampleService'] });
@ -127,6 +133,18 @@ describe('GrpcReflectionService', () => {
);
});
it('finds unscoped package types', () => {
const descriptors = reflectionService
.fileContainingSymbol('.TopLevelMessage')
.fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto);
const names = descriptors.map((desc) => desc.name);
assert.deepEqual(
new Set(names),
new Set(['root.proto']),
);
});
it('merges files based on package name', () => {
const descriptors = reflectionService
.fileContainingSymbol('vendor.CommonMessage')

View File

@ -7,8 +7,14 @@ describe('scope', () => {
assert.strictEqual(scope('grpc.health.v1.HealthCheckResponse.ServiceStatus'), 'grpc.health.v1.HealthCheckResponse');
assert.strictEqual(scope(scope(scope(scope('grpc.health.v1.HealthCheckResponse.ServiceStatus')))), 'grpc');
});
it('returns an empty package when at the top', () => {
assert.strictEqual(scope('Message'), '');
assert.strictEqual(scope(''), '');
});
it('handles globally scoped references', () => {
assert.strictEqual(scope('.Message'), '.');
assert.strictEqual(scope(scope('.Message')), '');
});
});