go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/web/rpcexplorer/src/data/prpc.tsx (about) 1 // Copyright 2022 The LUCI Authors. 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 16 // gRPC status codes (see google/rpc/code.proto). Kept as UPPER_CASE to simplify 17 // converting them to a canonical string representation via StatusCode[code]. 18 export enum StatusCode { 19 OK = 0, 20 CANCELLED = 1, 21 UNKNOWN = 2, 22 INVALID_ARGUMENT = 3, 23 DEADLINE_EXCEEDED = 4, 24 NOT_FOUND = 5, 25 ALREADY_EXISTS = 6, 26 PERMISSION_DENIED = 7, 27 RESOURCE_EXHAUSTED = 8, 28 FAILED_PRECONDITION = 9, 29 ABORTED = 10, 30 OUT_OF_RANGE = 11, 31 UNIMPLEMENTED = 12, 32 INTERNAL = 13, 33 UNAVAILABLE = 14, 34 DATA_LOSS = 15, 35 UNAUTHENTICATED = 16, 36 } 37 38 39 // An RPC error with a status code and an error message. 40 export class RPCError extends Error { 41 readonly code: StatusCode; 42 readonly http: number; 43 readonly text: string; 44 45 constructor(code: StatusCode, http: number, text: string) { 46 super(`${StatusCode[code]} (HTTP ${http}): ${text}`); 47 Object.setPrototypeOf(this, RPCError.prototype); 48 this.code = code; 49 this.http = http; 50 this.text = text; 51 } 52 } 53 54 55 // Descriptors holds all type information about RPC APIs exposed by a server. 56 export class Descriptors { 57 // A list of RPC services exposed by a server. 58 readonly services: Service[] = []; 59 // Helpers for visiting descriptors. 60 private readonly types: Types; 61 // A cache of visited message types. 62 private readonly messages: Cache<Message>; 63 // A cache of visited enum types. 64 private readonly enums: Cache<Enum>; 65 66 constructor(desc: FileDescriptorSet, services: string[]) { 67 for (const fileDesc of (desc.file ?? [])) { 68 resolveSourceLocation(fileDesc); // mutates `fileDesc` in-place 69 } 70 this.types = new Types(desc); 71 for (const serviceName of services) { 72 const desc = this.types.service(serviceName); 73 if (desc) { 74 this.services.push(new Service(serviceName, desc)); 75 } 76 } 77 this.messages = new Cache(); 78 this.enums = new Cache(); 79 } 80 81 // Returns a service by its name or undefined if no such service. 82 service(serviceName: string): Service | undefined { 83 return this.services.find((service) => service.name == serviceName); 84 } 85 86 // Returns a message descriptor given its name or undefined if not found. 87 message(messageName: string): Message | undefined { 88 return this.messages.get(messageName, () => { 89 const desc = this.types.message(messageName); 90 return desc ? new Message(desc, this) : undefined; 91 }); 92 } 93 94 // Returns an enum descriptor given its name or undefined if not found. 95 enum(enumName: string): Enum | undefined { 96 return this.enums.get(enumName, () => { 97 const desc = this.types.enum(enumName); 98 return desc ? new Enum(desc) : undefined; 99 }); 100 } 101 } 102 103 104 // Describes a structure of a message. 105 export class Message { 106 // True if this message represents some map<K,V> entry. 107 readonly mapEntry: boolean; 108 // The list of fields of this message. 109 readonly fields: Field[]; 110 111 constructor(desc: DescriptorProto, root: Descriptors) { 112 this.mapEntry = desc.options?.mapEntry ?? false; 113 this.fields = []; 114 for (const field of (desc.field ?? [])) { 115 this.fields.push(new Field(field, root)); 116 } 117 } 118 119 // Returns a field given its JSON name. 120 fieldByJsonName(name: string): Field | undefined { 121 for (const field of this.fields) { 122 if (field.jsonName == name) { 123 return field; 124 } 125 } 126 return undefined; 127 } 128 } 129 130 131 // Describes possible values of an enum. 132 export class Enum { 133 // Possible values of an enum. 134 readonly values: EnumValue[]; 135 136 constructor(desc: EnumDescriptorProto) { 137 this.values = []; 138 for (const val of (desc.value ?? [])) { 139 this.values.push(new EnumValue(val)); 140 } 141 } 142 } 143 144 145 // A single possible value of an enum 146 export class EnumValue { 147 // E.g. "SOME_VALUE"; 148 readonly name: string; 149 150 private readonly desc: EnumValueDescriptorProto; 151 private cachedDoc: string | null; 152 153 constructor(desc: EnumValueDescriptorProto) { 154 this.name = desc.name; 155 this.desc = desc; 156 this.cachedDoc = null; 157 } 158 159 // Paragraphs with documentation extracted from comments in the proto file. 160 get doc(): string { 161 if (this.cachedDoc == null) { 162 this.cachedDoc = extractDoc(this.desc.resolvedSourceLocation); 163 } 164 return this.cachedDoc; 165 } 166 } 167 168 169 // How field value is encoded in JSON. 170 export enum JSONType { 171 Object, 172 List, 173 String, 174 Scalar, // numbers, booleans, null 175 } 176 177 178 // Describes a structure of a single field. 179 export class Field { 180 // Fields JSON name, e.g. `someField`. 181 readonly jsonName: string; 182 // True if this is a repeated field or a map field. 183 readonly repeated: boolean; 184 185 private readonly desc: FieldDescriptorProto; 186 private readonly root: Descriptors; 187 private cachedDoc: string | null; 188 189 static readonly scalarTypeNames = new Map([ 190 ['TYPE_DOUBLE', 'double'], 191 ['TYPE_FLOAT', 'float'], 192 ['TYPE_INT64', 'int64'], 193 ['TYPE_UINT64', 'uint64'], 194 ['TYPE_INT32', 'int32'], 195 ['TYPE_FIXED64', 'fixed64'], 196 ['TYPE_FIXED32', 'fixed32'], 197 ['TYPE_BOOL', 'bool'], 198 ['TYPE_STRING', 'string'], 199 ['TYPE_BYTES', 'bytes'], 200 ['TYPE_UINT32', 'uint32'], 201 ['TYPE_SFIXED32', 'sfixed32'], 202 ['TYPE_SFIXED64', 'sfixed64'], 203 ['TYPE_SINT32', 'sint32'], 204 ['TYPE_SINT64', 'sint64'], 205 ]); 206 207 constructor(desc: FieldDescriptorProto, root: Descriptors) { 208 this.desc = desc; 209 this.root = root; 210 this.jsonName = desc.jsonName ?? ''; 211 this.repeated = desc.label == 'LABEL_REPEATED'; 212 this.cachedDoc = null; 213 } 214 215 // For message-valued fields, the type of the field value. 216 // 217 // Works for both singular and repeated fields. 218 get message(): Message | undefined { 219 if (this.desc.type == 'TYPE_MESSAGE') { 220 return this.root.message(this.desc.typeName ?? ''); 221 } 222 return undefined; 223 } 224 225 // For enum-valued fields, the type of the field value. 226 // 227 // Works for both singular and repeated fields. 228 get enum(): Enum | undefined { 229 if (this.desc.type == 'TYPE_ENUM') { 230 return this.root.enum(this.desc.typeName ?? ''); 231 } 232 return undefined; 233 } 234 235 // Field type name, approximately as it appears in the proto file. 236 // 237 // Recognizes repeated fields and maps. 238 get type(): string { 239 const pfx = this.repeated ? 'repeated ' : ''; 240 const scalar = Field.scalarTypeNames.get(this.desc.type); 241 if (scalar) { 242 return pfx + scalar; 243 } 244 const message = this.message; 245 if (message && message.mapEntry) { 246 // Note: mapEntry fields are always implicitly repeated, omit `pfx`. 247 return `map<${message.fields[0].type}, ${message.fields[1].type}>`; 248 } 249 return pfx + trimDot(this.desc.typeName ?? 'unknown'); 250 } 251 252 // Type of the field value in JSON representation. 253 // 254 // Recognizes repeated fields and maps. 255 get jsonType(): JSONType { 256 if (this.repeated) { 257 if (this.message?.mapEntry) { 258 return JSONType.Object; 259 } 260 return JSONType.List; 261 } 262 return this.jsonElementType; 263 } 264 265 // JSON type of the base element. 266 // 267 // For repeated fields it is the type inside the list. 268 get jsonElementType(): JSONType { 269 switch (this.desc.type) { 270 case 'TYPE_MESSAGE': 271 return JSONType.Object; 272 case 'TYPE_ENUM': 273 case 'TYPE_STRING': 274 case 'TYPE_BYTES': 275 return JSONType.String; 276 default: 277 return JSONType.Scalar; 278 } 279 } 280 281 // Paragraphs with documentation extracted from comments in the proto file. 282 get doc(): string { 283 if (this.cachedDoc == null) { 284 this.cachedDoc = extractDoc(this.desc.resolvedSourceLocation); 285 } 286 return this.cachedDoc; 287 } 288 } 289 290 291 // Descriptions of a single RPC service. 292 export class Service { 293 // Name of the service, e.g. `discovery.Discovery`. 294 readonly name: string; 295 // The last component of the name, e.g. `Discovery`. 296 readonly title: string; 297 // Short description e.g. `Describes services.`. 298 readonly help: string; 299 // Paragraphs with full documentation. 300 readonly doc: string; 301 // List of methods exposed by the service. 302 readonly methods: Method[] = []; 303 304 constructor(name: string, desc: ServiceDescriptorProto) { 305 this.name = name; 306 this.title = splitFullName(name)[1]; 307 this.help = extractHelp(desc.resolvedSourceLocation, this.title, 'service'); 308 this.doc = extractDoc(desc.resolvedSourceLocation); 309 if (desc.method !== undefined) { 310 for (const methodDesc of desc.method) { 311 this.methods.push(new Method(this.name, methodDesc)); 312 } 313 } 314 } 315 316 // Returns a method by its name or undefined if no such method. 317 method(methodName: string): Method | undefined { 318 return this.methods.find((method) => method.name == methodName); 319 } 320 } 321 322 323 // Method describes a single RPC method. 324 export class Method { 325 // Service name the method belongs to. 326 readonly service: string; 327 // Name of the method, e.g. `Describe `. 328 readonly name: string; 329 // Short description e.g. `Returns ...`. 330 readonly help: string; 331 // Paragraphs with full documentation. 332 readonly doc: string; 333 // Name of the protobuf message type of the request. 334 readonly requestType: string; 335 336 constructor(service: string, desc: MethodDescriptorProto) { 337 this.service = service; 338 this.name = desc.name; 339 this.help = extractHelp(desc.resolvedSourceLocation, desc.name, 'method'); 340 this.doc = extractDoc(desc.resolvedSourceLocation); 341 this.requestType = desc.inputType; 342 } 343 344 // Invokes this method, returns prettified JSON response as a string. 345 // 346 // `authorization` will be used as a value of Authorization header. `traceID` 347 // is used to populate X-Cloud-Trace-Context header. 348 async invoke( 349 request: string, 350 authorization: string, 351 traceID?: string, 352 ): Promise<string> { 353 const resp: object = await invokeMethod( 354 this.service, this.name, request, authorization, traceID, 355 ); 356 return JSON.stringify(resp, null, 2); 357 } 358 } 359 360 361 // Loads RPC API descriptors from the server. 362 export const loadDescriptors = async (): Promise<Descriptors> => { 363 const resp: DescribeResponse = await invokeMethod( 364 'discovery.Discovery', 'Describe', '{}', '', 365 ); 366 return new Descriptors(resp.description, resp.services); 367 }; 368 369 370 // Generates a tracing ID for Cloud Trace. 371 // 372 // See https://cloud.google.com/trace/docs/setup#force-trace. 373 export const generateTraceID = (): string => { 374 let output = ''; 375 for (let i = 0; i < 32; ++i) { 376 output += (Math.floor(Math.random() * 16)).toString(16); 377 } 378 return output; 379 }; 380 381 382 // Private guts. 383 384 385 // A helper to cache visited types. 386 class Cache<V> { 387 private cache: Map<string, V | 'none'>; 388 389 constructor() { 390 this.cache = new Map(); 391 } 392 393 get(key: string, val: () => V | undefined): V | undefined { 394 let cached = this.cache.get(key); 395 switch (cached) { 396 case 'none': 397 // Cached "absence". 398 return undefined; 399 case undefined: 400 // Not in the cache yet. 401 cached = val(); 402 this.cache.set(key, cached == undefined ? 'none' : cached); 403 return cached; 404 default: 405 return cached; 406 } 407 } 408 } 409 410 411 // Types knows how to look up proto descriptors given full proto type names. 412 class Types { 413 // Proto package name => list of FileDescriptorProto where it is defined. 414 private packageMap = new Map<string, FileDescriptorProto[]>(); 415 416 constructor(desc: FileDescriptorSet) { 417 for (const fileDesc of (desc.file ?? [])) { 418 const descs = this.packageMap.get(fileDesc.package); 419 if (descs === undefined) { 420 this.packageMap.set(fileDesc.package, [fileDesc]); 421 } else { 422 descs.push(fileDesc); 423 } 424 } 425 } 426 427 // Given a full service name returns its descriptor or undefined. 428 service(fullName: string): ServiceDescriptorProto | undefined { 429 const [pkg, name] = splitFullName(fullName); 430 return this.visitPackage(pkg, (fileDesc) => { 431 for (const svc of (fileDesc.service ?? [])) { 432 if (svc.name == name) { 433 return svc; 434 } 435 } 436 return undefined; 437 }); 438 } 439 440 // Given a full message name returns its descriptor or undefined. 441 message(fullName: string): DescriptorProto | undefined { 442 // Assume this is a top-level type defined in some package. 443 const [pkg, name] = splitFullName(fullName); 444 const found = this.visitPackage(pkg, (fileDesc) => { 445 for (const desc of (fileDesc.messageType ?? [])) { 446 if (desc.name == name) { 447 return desc; 448 } 449 } 450 return undefined; 451 }); 452 if (found) { 453 return found; 454 } 455 // It might be a reference to a nested type, in which case `pkg` is actually 456 // some message name itself. Try to find it. 457 const parent = this.message(pkg); 458 if (parent) { 459 for (const desc of (parent.nestedType ?? [])) { 460 if (desc.name == name) { 461 return desc; 462 } 463 } 464 } 465 return undefined; 466 } 467 468 // Given a full enum name returns its descriptor or undefined. 469 enum(fullName: string): EnumDescriptorProto | undefined { 470 // Assume this is a top-level type defined in some package. 471 const [pkg, name] = splitFullName(fullName); 472 const found = this.visitPackage(pkg, (fileDesc) => { 473 for (const msg of (fileDesc.enumType ?? [])) { 474 if (msg.name == name) { 475 return msg; 476 } 477 } 478 return undefined; 479 }); 480 if (found) { 481 return found; 482 } 483 // It might be a reference to a nested type, in which case `pkg` is actually 484 // some message name. Try to find it. 485 const parent = this.message(pkg); 486 if (parent) { 487 for (const msg of (parent.enumType ?? [])) { 488 if (msg.name == name) { 489 return msg; 490 } 491 } 492 } 493 return undefined; 494 } 495 496 // Visits all FileDescriptorProto that define a particular package. 497 private visitPackage<T>( 498 pkg: string, 499 cb: (desc: FileDescriptorProto) => T | undefined, 500 ): T | undefined { 501 const descs = this.packageMap.get(pkg); 502 if (descs !== undefined) { 503 for (const desc of descs) { 504 const res = cb(desc); 505 if (res !== undefined) { 506 return res; 507 } 508 } 509 } 510 return undefined; 511 } 512 } 513 514 515 // resolveSourceLocation recursively populates `resolvedSourceLocation`. 516 // 517 // See https://github.com/protocolbuffers/protobuf/blob/5a5683/src/google/protobuf/descriptor.proto#L803 518 const resolveSourceLocation = (desc: FileDescriptorProto) => { 519 if (!desc.sourceCodeInfo) { 520 return; 521 } 522 523 // A helper to convert a location path to a string key. 524 const locationKey = (path: number[]): string => { 525 return path.map((n) => n.toString()).join('.'); 526 }; 527 528 // Build a map {path -> SourceCodeInfoLocation}. See the link above for 529 // what a path is. 530 const locationMap = new Map<string, SourceCodeInfoLocation>(); 531 for (const location of (desc.sourceCodeInfo.location ?? [])) { 532 if (location.path) { 533 locationMap.set(locationKey(location.path), location); 534 } 535 } 536 537 // Now recursively traverse the tree of definitions in the `desc` and 538 // for each visited path see if there's a location for it in `locationMap`. 539 // 540 // Various magical numbers below are message field numbers defined in 541 // descriptor.proto (again, see the link above, it is a pretty confusing 542 // mechanism and comments in descriptor.proto are the best explanation). 543 544 // Helper for recursively diving into a list of definitions of some kind. 545 const resolveList = <T extends HasResolved>( 546 path: number[], 547 field: number, 548 list: T[] | undefined, 549 dive?: (path: number[], val: T) => void, 550 ) => { 551 if (!list) { 552 return; 553 } 554 path.push(field); 555 for (const [idx, val] of list.entries()) { 556 path.push(idx); 557 val.resolvedSourceLocation = locationMap.get(locationKey(path)); 558 if (dive) { 559 dive(path, val); 560 } 561 path.pop(); 562 } 563 path.pop(); 564 }; 565 566 const resolveService = (path: number[], msg: ServiceDescriptorProto) => { 567 resolveList(path, 2, msg.method); 568 }; 569 570 const resolveEnum = (path: number[], msg: EnumDescriptorProto) => { 571 resolveList(path, 2, msg.value); 572 }; 573 574 const resolveMessage = (path: number[], msg: DescriptorProto) => { 575 resolveList(path, 2, msg.field); 576 resolveList(path, 3, msg.nestedType, resolveMessage); 577 resolveList(path, 4, msg.enumType, resolveEnum); 578 }; 579 580 // Root lists. 581 resolveList([], 4, desc.messageType, resolveMessage); 582 resolveList([], 5, desc.enumType, resolveEnum); 583 resolveList([], 6, desc.service, resolveService); 584 }; 585 586 587 // extractHelp extracts a first paragraph of the leading comment and trims it 588 // a bit if necessary. 589 // 590 // A paragraph is delimited by '\n\n' or '. '. 591 const extractHelp = ( 592 loc: SourceCodeInfoLocation | undefined, 593 subject: string, 594 type: string, 595 ): string => { 596 let comment = (loc ? loc.leadingComments ?? '' : '').trim(); 597 if (!comment) { 598 return ''; 599 } 600 601 // Get the first paragraph. 602 comment = splitParagraph(comment)[0]; 603 604 // Go-based services very often have leading comments that start with 605 // "ServiceName service does blah". We convert this to just "Does blah", 606 // since we already show the service name prominently and this duplication 607 // looks bad. 608 const [trimmed, yes] = trimWord(comment, subject); 609 if (yes) { 610 const evenMore = trimWord(trimmed, type)[0]; 611 if (evenMore) { 612 comment = evenMore.charAt(0).toUpperCase() + evenMore.substring(1); 613 } 614 } 615 616 return deindent(comment); 617 }; 618 619 620 // extractDoc converts comments into a list of paragraphs separated by '\n\n'. 621 const extractDoc = (loc: SourceCodeInfoLocation | undefined): string => { 622 let text = (loc ? loc.leadingComments ?? '' : '').trim(); 623 const paragraphs: string[] = []; 624 while (text) { 625 const [p, remains] = splitParagraph(text); 626 if (p) { 627 paragraphs.push(deindent(p)); 628 } 629 text = remains; 630 } 631 return paragraphs.join('\n\n'); 632 }; 633 634 635 // splitParagraph returns the first paragraph and what's left. 636 const splitParagraph = (s: string): [string, string] => { 637 let idx = s.indexOf('\n\n'); 638 if (idx == -1) { 639 idx = s.indexOf('. '); 640 if (idx != -1) { 641 idx += 1; // include the dot 642 } 643 } 644 if (idx != -1) { 645 return [s.substring(0, idx).trim(), s.substring(idx).trim()]; 646 } 647 return [s, '']; 648 }; 649 650 651 // deindent removes a leading space from all lines that have it. 652 // 653 // Protobuf docs are generated from comments not unlike this one. `protoc` 654 // strips `//` but does nothing to spaces that follow `//`. As a result docs are 655 // intended by one space. 656 const deindent = (s: string): string => { 657 const lines = s.split('\n'); 658 for (let i = 0; i < lines.length; i++) { 659 if (lines[i].startsWith(' ')) { 660 lines[i] = lines[i].substring(1); 661 } 662 } 663 return lines.join('\n'); 664 }; 665 666 667 // trimWord removes a leading word from a sentence, kind of. 668 const trimWord = (s: string, word: string): [string, boolean] => { 669 if (s.startsWith(word + ' ')) { 670 return [s.substring(word.length + 1).trimLeft(), true]; 671 } 672 return [s, false]; 673 }; 674 675 676 // ".google.protobuf.Empty" => "google.protobuf.Empty"; 677 const trimDot = (name: string): string => { 678 if (name.startsWith('.')) { 679 return name.substring(1); 680 } 681 return name; 682 }; 683 684 685 // ".google.protobuf.Empty" => ["google.protobuf", "Empty"]. 686 const splitFullName = (fullName: string): [string, string] => { 687 const lastDot = fullName.lastIndexOf('.'); 688 const start = fullName.startsWith('.') ? 1 : 0; 689 return [ 690 fullName.substring(start, lastDot), 691 fullName.substring(lastDot + 1), 692 ]; 693 }; 694 695 696 // invokeMethod sends a pRPC request and parses the response. 697 const invokeMethod = async <T, >( 698 service: string, 699 method: string, 700 body: string, 701 authorization: string, 702 traceID?: string, 703 ): Promise<T> => { 704 const headers = new Headers(); 705 headers.set('Accept', 'application/json; charset=utf-8'); 706 headers.set('Content-Type', 'application/json; charset=utf-8'); 707 if (authorization) { 708 headers.set('Authorization', authorization); 709 } 710 if (traceID) { 711 headers.set('X-Cloud-Trace-Context', `${traceID}/1;o=1`); 712 } 713 714 const response = await fetch(`/prpc/${service}/${method}`, { 715 method: 'POST', 716 credentials: 'omit', 717 headers: headers, 718 body: body, 719 }); 720 let responseBody = await response.text(); 721 722 // Trim the magic anti-XSS prefix if present. 723 const pfx = ')]}\''; 724 if (responseBody.startsWith(pfx)) { 725 responseBody = responseBody.slice(pfx.length); 726 } 727 728 // Parse gRPC status code header if available. 729 let grpcCode = StatusCode.UNKNOWN; 730 const codeStr = response.headers.get('X-Prpc-Grpc-Code'); 731 if (codeStr) { 732 const num = parseInt(codeStr); 733 if (isNaN(num) || num < 0 || num > 16) { 734 grpcCode = StatusCode.UNKNOWN; 735 } else { 736 grpcCode = num; 737 } 738 } else if (response.status >= 500) { 739 grpcCode = StatusCode.INTERNAL; 740 } 741 742 if (grpcCode != StatusCode.OK) { 743 throw new RPCError(grpcCode, response.status, responseBody.trim()); 744 } 745 746 return JSON.parse(responseBody) as T; 747 }; 748 749 750 // See go.chromium.org/luci/grpc/discovery/service.proto and protos it imports 751 // (recursively). 752 // 753 // Only fields used by RPC Explorer are exposed below, using their JSONPB names. 754 // 755 // To simplify extraction of code comments for various definitions we also add 756 // an extra field `resolvedSourceLocation` to some interfaces. Its value is 757 // derived from `sourceCodeInfo` in resolveSourceLocation. 758 759 // Implemented by descriptors that have resolve source location attached. 760 interface HasResolved { 761 resolvedSourceLocation?: SourceCodeInfoLocation; 762 } 763 764 interface DescribeResponse { 765 description: FileDescriptorSet; 766 services: string[]; 767 } 768 769 export interface FileDescriptorSet { 770 file?: FileDescriptorProto[]; 771 } 772 773 interface FileDescriptorProto { 774 name: string; 775 package: string; 776 777 messageType?: DescriptorProto[]; // = 4 778 enumType?: EnumDescriptorProto[]; // = 5; 779 service?: ServiceDescriptorProto[]; // = 6 780 781 sourceCodeInfo?: SourceCodeInfo; 782 } 783 784 interface DescriptorProto extends HasResolved { 785 name: string; 786 field?: FieldDescriptorProto[]; // = 2 787 nestedType?: DescriptorProto[]; // = 3 788 enumType?: EnumDescriptorProto[]; // = 4 789 options?: MessageOptions; 790 } 791 792 interface FieldDescriptorProto extends HasResolved { 793 name: string; 794 jsonName?: string; 795 type: string; // e.g. "TYPE_INT32" 796 typeName?: string; // for message and enum types only 797 label: string; // e.g. "LABEL_REPEATED" 798 } 799 800 interface EnumDescriptorProto extends HasResolved { 801 name: string; 802 value?: EnumValueDescriptorProto[]; // = 2; 803 } 804 805 interface EnumValueDescriptorProto extends HasResolved { 806 name: string; 807 number: number; 808 } 809 810 interface ServiceDescriptorProto extends HasResolved { 811 name: string; 812 method?: MethodDescriptorProto[]; // = 2 813 } 814 815 interface MethodDescriptorProto extends HasResolved { 816 name: string; 817 818 inputType: string; 819 outputType: string; 820 } 821 822 interface MessageOptions { 823 mapEntry?: boolean; 824 } 825 826 interface SourceCodeInfo { 827 location?: SourceCodeInfoLocation[]; 828 } 829 830 interface SourceCodeInfoLocation { 831 path: number[]; 832 leadingComments?: string; 833 }