├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── example.proto ├── index.js ├── package.json ├── parse.js ├── stringify.js ├── test ├── fixtures │ ├── basic.json │ ├── basic.proto │ ├── comments.json │ ├── comments.proto │ ├── complex.json │ ├── complex.proto │ ├── enum.json │ ├── enum.proto │ ├── escaped-quotes.json │ ├── escaped-quotes.proto │ ├── extend.json │ ├── extend.proto │ ├── import.json │ ├── import.proto │ ├── map.json │ ├── map.proto │ ├── no-tags.proto │ ├── oneof.json │ ├── oneof.proto │ ├── option.json │ ├── option.proto │ ├── options.json │ ├── options.proto │ ├── pheromon-trajectories.proto │ ├── reserved.json │ ├── reserved.proto │ ├── search.json │ ├── search.proto │ ├── service.json │ ├── service.proto │ ├── valid-packed.proto │ ├── version.json │ └── version.proto └── index.js └── tokenize.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .editorconfig -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | - '5' 7 | - 'node' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protocol-buffers-schema 2 | 3 | No nonsense [protocol buffers](https://developers.google.com/protocol-buffers) schema parser written in Javascript 4 | 5 | ``` js 6 | npm install protocol-buffers-schema 7 | ``` 8 | 9 | [![build status](http://img.shields.io/travis/mafintosh/protocol-buffers-schema.svg?style=flat)](http://travis-ci.org/mafintosh/protocol-buffers-schema) 10 | 11 | ## Usage 12 | 13 | First save the following file as `example.proto` 14 | 15 | ```proto 16 | syntax = "proto2"; 17 | 18 | message Point { 19 | required int32 x = 1; 20 | required int32 y=2; 21 | optional string label = 3; 22 | } 23 | 24 | message Line { 25 | required Point start = 1; 26 | required Point end = 2; 27 | optional string label = 3; 28 | } 29 | ``` 30 | 31 | The run the following example 32 | 33 | ``` js 34 | var fs = require('fs') 35 | var schema = require('protocol-buffers-schema') 36 | 37 | // pass a buffer or string to schema.parse 38 | var sch = schema.parse(fs.readFileSync('example.proto')) 39 | 40 | // will print out the schema as a javascript object 41 | console.log(sch) 42 | ``` 43 | 44 | Running the above example will print something like 45 | 46 | ``` js 47 | { 48 | syntax: 2, 49 | package: null, 50 | enums: [], 51 | messages: [{ 52 | name: 'Point', 53 | enums: [], 54 | messages: [], 55 | options: {}, 56 | fields: [{ 57 | name: 'x', 58 | type: 'int32', 59 | tag: 1, 60 | required: true, 61 | repeated: false, 62 | options: {} 63 | }, { 64 | name: 'y', 65 | type: 'int32', 66 | tag: 2, 67 | required: true, 68 | repeated: false, 69 | options: {} 70 | }, { 71 | name: 'label', 72 | type: 'string', 73 | tag: 3, 74 | required: false, 75 | repeated: false, 76 | options: {} 77 | }] 78 | }, { 79 | name: 'Line', 80 | enums: [], 81 | messages: [], 82 | options: {}, 83 | fields: [{ 84 | name: 'start', 85 | type: 'Point', 86 | tag: 1, 87 | required: true, 88 | repeated: false, 89 | options: {} 90 | }, { 91 | name: 'end', 92 | type: 'Point', 93 | tag: 2, 94 | required: true, 95 | repeated: false, 96 | options: {} 97 | }, { 98 | name: 'label', 99 | type: 'string', 100 | tag: 3, 101 | required: false, 102 | repeated: false, 103 | options: {} 104 | }] 105 | }], 106 | options:{} 107 | } 108 | ``` 109 | 110 | ## API 111 | 112 | #### `schema.parse(protobufSchemaBufferOrString)` 113 | 114 | Parses a .proto schema into a javascript object 115 | 116 | #### `schema.stringify(schema)` 117 | 118 | Stringifies a parsed schema back into .proto format 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var schema = require('./') 2 | var fs = require('fs') 3 | 4 | var sch = schema.parse(fs.readFileSync('example.proto')) 5 | 6 | console.log('Parsed schema:') 7 | console.log(JSON.stringify(sch, null, 2)) 8 | console.log('') 9 | 10 | console.log('Stringified schema:') 11 | console.log(schema.stringify(sch)) 12 | -------------------------------------------------------------------------------- /example.proto: -------------------------------------------------------------------------------- 1 | // example file 2 | 3 | message Test { 4 | map data = 1; 5 | required string hello = 2; 6 | oneof test { 7 | uint32 age = 3; 8 | uint32 year = 4; 9 | } 10 | message Nested { 11 | optional bytes thing = 1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var parse = require('./parse') 2 | var stringify = require('./stringify') 3 | 4 | module.exports = parse 5 | module.exports.parse = parse 6 | module.exports.stringify = stringify 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protocol-buffers-schema", 3 | "version": "3.6.0", 4 | "description": "No nonsense protocol buffers schema parser written in Javascript", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "standard": "^10.0.3", 8 | "tape": "^4.8.0" 9 | }, 10 | "scripts": { 11 | "test": "standard && tape test/*.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mafintosh/protocol-buffers-schema" 16 | }, 17 | "keywords": [ 18 | "protobuf", 19 | "protocol", 20 | "buffers", 21 | "schema", 22 | "parser", 23 | "parse" 24 | ], 25 | "author": "Mathias Buus", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mafintosh/protocol-buffers-schema/issues" 29 | }, 30 | "homepage": "https://github.com/mafintosh/protocol-buffers-schema" 31 | } 32 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | var tokenize = require('./tokenize') 2 | var MAX_RANGE = 0x1FFFFFFF 3 | 4 | // "Only repeated fields of primitive numeric types (types which use the varint, 32-bit, or 64-bit wire types) can be declared "packed"." 5 | // https://developers.google.com/protocol-buffers/docs/encoding#optional 6 | var PACKABLE_TYPES = [ 7 | // varint wire types 8 | 'int32', 'int64', 'uint32', 'uint64', 'sint32', 'sint64', 'bool', 9 | // + ENUMS 10 | // 64-bit wire types 11 | 'fixed64', 'sfixed64', 'double', 12 | // 32-bit wire types 13 | 'fixed32', 'sfixed32', 'float' 14 | ] 15 | 16 | var onfieldoptionvalue = function (tokens) { 17 | var value = tokens.shift() 18 | if (value !== '{') { 19 | return value 20 | } 21 | value = {} 22 | var field = '' 23 | while (tokens.length) { 24 | switch (tokens[0]) { 25 | case '}': 26 | tokens.shift() 27 | return value 28 | case ':': 29 | tokens.shift() 30 | value[field] = onfieldoptionvalue(tokens) 31 | break 32 | default: 33 | field = tokens.shift() 34 | } 35 | } 36 | } 37 | 38 | var onfieldoptions = function (tokens) { 39 | var opts = {} 40 | 41 | while (tokens.length) { 42 | switch (tokens[0]) { 43 | case '[': 44 | case ',': { 45 | tokens.shift() 46 | var name = tokens.shift() 47 | if (name === '(') { // handling [(A) = B] 48 | name = tokens.shift() 49 | tokens.shift() // remove the end of bracket 50 | } 51 | var field = [] 52 | if (tokens[0][0] === '.') { 53 | field = tokens[0].substr(1).split('.') 54 | tokens.shift() 55 | } 56 | if (tokens[0] !== '=') throw new Error('Unexpected token in field options: ' + tokens[0]) 57 | tokens.shift() 58 | if (tokens[0] === ']') throw new Error('Unexpected ] in field option') 59 | 60 | // for option (A).b.c 61 | // path will be ['A', 'b'] and lastFieldName 'c' 62 | var path = [name].concat(field) 63 | var lastFieldName = path.pop() 64 | 65 | // opt references opts.A.b 66 | var opt = path.reduce(function (opt, n, index) { 67 | if (opt[n] == null) { 68 | opt[n] = {} 69 | } 70 | return opt[n] 71 | }, opts) 72 | 73 | // now set opt['c'] that references opts.A.b['c'] 74 | opt[lastFieldName] = onfieldoptionvalue(tokens) 75 | break 76 | } 77 | case ']': 78 | tokens.shift() 79 | return opts 80 | 81 | default: 82 | throw new Error('Unexpected token in field options: ' + tokens[0]) 83 | } 84 | } 85 | 86 | throw new Error('No closing tag for field options') 87 | } 88 | 89 | var onfield = function (tokens) { 90 | var field = { 91 | name: null, 92 | type: null, 93 | tag: -1, 94 | map: null, 95 | oneof: null, 96 | required: false, 97 | repeated: false, 98 | options: {} 99 | } 100 | 101 | while (tokens.length) { 102 | switch (tokens[0]) { 103 | case '=': 104 | tokens.shift() 105 | field.tag = Number(tokens.shift()) 106 | break 107 | 108 | case 'map': 109 | field.type = 'map' 110 | field.map = { from: null, to: null } 111 | tokens.shift() 112 | if (tokens[0] !== '<') throw new Error('Unexpected token in map type: ' + tokens[0]) 113 | tokens.shift() 114 | field.map.from = tokens.shift() 115 | if (tokens[0] !== ',') throw new Error('Unexpected token in map type: ' + tokens[0]) 116 | tokens.shift() 117 | field.map.to = tokens.shift() 118 | if (tokens[0] !== '>') throw new Error('Unexpected token in map type: ' + tokens[0]) 119 | tokens.shift() 120 | field.name = tokens.shift() 121 | break 122 | 123 | case 'repeated': 124 | case 'required': 125 | case 'optional': 126 | var t = tokens.shift() 127 | field.required = t === 'required' 128 | field.repeated = t === 'repeated' 129 | field.type = tokens.shift() 130 | field.name = tokens.shift() 131 | break 132 | 133 | case '[': 134 | field.options = onfieldoptions(tokens) 135 | break 136 | 137 | case ';': 138 | if (field.name === null) throw new Error('Missing field name') 139 | if (field.type === null) throw new Error('Missing type in message field: ' + field.name) 140 | if (field.tag === -1) throw new Error('Missing tag number in message field: ' + field.name) 141 | tokens.shift() 142 | return field 143 | 144 | default: 145 | throw new Error('Unexpected token in message field: ' + tokens[0]) 146 | } 147 | } 148 | 149 | throw new Error('No ; found for message field') 150 | } 151 | 152 | var onmessagebody = function (tokens) { 153 | var body = { 154 | enums: [], 155 | options: {}, 156 | messages: [], 157 | fields: [], 158 | extends: [], 159 | extensions: null 160 | } 161 | 162 | while (tokens.length) { 163 | switch (tokens[0]) { 164 | case 'map': 165 | case 'repeated': 166 | case 'optional': 167 | case 'required': 168 | body.fields.push(onfield(tokens)) 169 | break 170 | 171 | case 'enum': 172 | body.enums.push(onenum(tokens)) 173 | break 174 | 175 | case 'message': 176 | body.messages.push(onmessage(tokens)) 177 | break 178 | 179 | case 'extensions': 180 | body.extensions = onextensions(tokens) 181 | break 182 | 183 | case 'oneof': 184 | tokens.shift() 185 | var name = tokens.shift() 186 | if (tokens[0] !== '{') throw new Error('Unexpected token in oneof: ' + tokens[0]) 187 | tokens.shift() 188 | while (tokens[0] !== '}') { 189 | tokens.unshift('optional') 190 | var field = onfield(tokens) 191 | field.oneof = name 192 | body.fields.push(field) 193 | } 194 | tokens.shift() 195 | break 196 | 197 | case 'extend': 198 | body.extends.push(onextend(tokens)) 199 | break 200 | 201 | case ';': 202 | tokens.shift() 203 | break 204 | 205 | case 'reserved': 206 | tokens.shift() 207 | while (tokens[0] !== ';') { 208 | tokens.shift() 209 | } 210 | break 211 | 212 | case 'option': 213 | var opt = onoption(tokens) 214 | if (body.options[opt.name] !== undefined) throw new Error('Duplicate option ' + opt.name) 215 | body.options[opt.name] = opt.value 216 | break 217 | 218 | default: 219 | // proto3 does not require the use of optional/required, assumed as optional 220 | // "singular: a well-formed message can have zero or one of this field (but not more than one)." 221 | // https://developers.google.com/protocol-buffers/docs/proto3#specifying-field-rules 222 | tokens.unshift('optional') 223 | body.fields.push(onfield(tokens)) 224 | } 225 | } 226 | 227 | return body 228 | } 229 | 230 | var onextend = function (tokens) { 231 | var out = { 232 | name: tokens[1], 233 | message: onmessage(tokens) 234 | } 235 | return out 236 | } 237 | 238 | var onextensions = function (tokens) { 239 | tokens.shift() 240 | var from = Number(tokens.shift()) 241 | if (isNaN(from)) throw new Error('Invalid from in extensions definition') 242 | if (tokens.shift() !== 'to') throw new Error("Expected keyword 'to' in extensions definition") 243 | var to = tokens.shift() 244 | if (to === 'max') to = MAX_RANGE 245 | to = Number(to) 246 | if (isNaN(to)) throw new Error('Invalid to in extensions definition') 247 | if (tokens.shift() !== ';') throw new Error('Missing ; in extensions definition') 248 | return { from: from, to: to } 249 | } 250 | var onmessage = function (tokens) { 251 | tokens.shift() 252 | 253 | var lvl = 1 254 | var body = [] 255 | var msg = { 256 | name: tokens.shift(), 257 | options: {}, 258 | enums: [], 259 | extends: [], 260 | messages: [], 261 | fields: [] 262 | } 263 | 264 | if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0]) 265 | tokens.shift() 266 | 267 | while (tokens.length) { 268 | if (tokens[0] === '{') lvl++ 269 | else if (tokens[0] === '}') lvl-- 270 | 271 | if (!lvl) { 272 | tokens.shift() 273 | body = onmessagebody(body) 274 | msg.enums = body.enums 275 | msg.messages = body.messages 276 | msg.fields = body.fields 277 | msg.extends = body.extends 278 | msg.extensions = body.extensions 279 | msg.options = body.options 280 | return msg 281 | } 282 | 283 | body.push(tokens.shift()) 284 | } 285 | 286 | if (lvl) throw new Error('No closing tag for message') 287 | } 288 | 289 | var onpackagename = function (tokens) { 290 | tokens.shift() 291 | var name = tokens.shift() 292 | if (tokens[0] !== ';') throw new Error('Expected ; but found ' + tokens[0]) 293 | tokens.shift() 294 | return name 295 | } 296 | 297 | var onsyntaxversion = function (tokens) { 298 | tokens.shift() 299 | 300 | if (tokens[0] !== '=') throw new Error('Expected = but found ' + tokens[0]) 301 | tokens.shift() 302 | 303 | var version = tokens.shift() 304 | switch (version) { 305 | case '"proto2"': 306 | version = 2 307 | break 308 | 309 | case '"proto3"': 310 | version = 3 311 | break 312 | 313 | default: 314 | throw new Error('Expected protobuf syntax version but found ' + version) 315 | } 316 | 317 | if (tokens[0] !== ';') throw new Error('Expected ; but found ' + tokens[0]) 318 | tokens.shift() 319 | 320 | return version 321 | } 322 | 323 | var onenumvalue = function (tokens) { 324 | if (tokens.length < 4) throw new Error('Invalid enum value: ' + tokens.slice(0, 3).join(' ')) 325 | if (tokens[0] === 'reserved') { 326 | tokens.shift() 327 | while (tokens[0] !== ';') { 328 | tokens.shift() 329 | } 330 | tokens.shift() 331 | return null 332 | } 333 | if (tokens[1] !== '=') throw new Error('Expected = but found ' + tokens[1]) 334 | if (tokens[3] !== ';' && tokens[3] !== '[') throw new Error('Expected ; or [ but found ' + tokens[1]) 335 | 336 | var name = tokens.shift() 337 | tokens.shift() 338 | var val = { 339 | value: null, 340 | options: {} 341 | } 342 | val.value = Number(tokens.shift()) 343 | if (tokens[0] === '[') { 344 | val.options = onfieldoptions(tokens) 345 | } 346 | tokens.shift() // expecting the semicolon here 347 | 348 | return { 349 | name: name, 350 | val: val 351 | } 352 | } 353 | 354 | var onenum = function (tokens) { 355 | tokens.shift() 356 | var options = {} 357 | var e = { 358 | name: tokens.shift(), 359 | values: {}, 360 | options: {} 361 | } 362 | 363 | if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0]) 364 | tokens.shift() 365 | 366 | while (tokens.length) { 367 | if (tokens[0] === '}') { 368 | tokens.shift() 369 | // there goes optional semicolon after the enclosing "}" 370 | if (tokens[0] === ';') tokens.shift() 371 | return e 372 | } 373 | if (tokens[0] === 'option') { 374 | options = onoption(tokens) 375 | e.options[options.name] = options.value 376 | continue 377 | } 378 | var val = onenumvalue(tokens) 379 | if (val !== null) { 380 | e.values[val.name] = val.val 381 | } 382 | } 383 | 384 | throw new Error('No closing tag for enum') 385 | } 386 | 387 | var onoption = function (tokens) { 388 | var name = null 389 | var value = null 390 | 391 | var parse = function (value) { 392 | if (value === 'true') return true 393 | if (value === 'false') return false 394 | return value.replace(/^"+|"+$/gm, '') 395 | } 396 | 397 | while (tokens.length) { 398 | if (tokens[0] === ';') { 399 | tokens.shift() 400 | return { name: name, value: value } 401 | } 402 | switch (tokens[0]) { 403 | case 'option': 404 | tokens.shift() 405 | 406 | var hasBracket = tokens[0] === '(' 407 | if (hasBracket) tokens.shift() 408 | 409 | name = tokens.shift() 410 | 411 | if (hasBracket) { 412 | if (tokens[0] !== ')') throw new Error('Expected ) but found ' + tokens[0]) 413 | tokens.shift() 414 | } 415 | 416 | if (tokens[0][0] === '.') { 417 | name += tokens.shift() 418 | } 419 | 420 | break 421 | 422 | case '=': 423 | tokens.shift() 424 | if (name === null) throw new Error('Expected key for option with value: ' + tokens[0]) 425 | value = parse(tokens.shift()) 426 | 427 | if (name === 'optimize_for' && !/^(SPEED|CODE_SIZE|LITE_RUNTIME)$/.test(value)) { 428 | throw new Error('Unexpected value for option optimize_for: ' + value) 429 | } else if (value === '{') { 430 | // option foo = {bar: baz} 431 | value = onoptionMap(tokens) 432 | } 433 | break 434 | 435 | default: 436 | throw new Error('Unexpected token in option: ' + tokens[0]) 437 | } 438 | } 439 | } 440 | 441 | var onoptionMap = function (tokens) { 442 | var parse = function (value) { 443 | if (value === 'true') return true 444 | if (value === 'false') return false 445 | return value.replace(/^"+|"+$/gm, '') 446 | } 447 | 448 | var map = {} 449 | 450 | while (tokens.length) { 451 | if (tokens[0] === '}') { 452 | tokens.shift() 453 | return map 454 | } 455 | 456 | var hasBracket = tokens[0] === '(' 457 | if (hasBracket) tokens.shift() 458 | 459 | var key = tokens.shift() 460 | if (hasBracket) { 461 | if (tokens[0] !== ')') throw new Error('Expected ) but found ' + tokens[0]) 462 | tokens.shift() 463 | } 464 | 465 | var value = null 466 | 467 | switch (tokens[0]) { 468 | case ':': 469 | if (map[key] !== undefined) throw new Error('Duplicate option map key ' + key) 470 | 471 | tokens.shift() 472 | 473 | value = parse(tokens.shift()) 474 | 475 | if (value === '{') { 476 | // option foo = {bar: baz} 477 | value = onoptionMap(tokens) 478 | } 479 | 480 | map[key] = value 481 | 482 | if (tokens[0] === ';') { 483 | tokens.shift() 484 | } 485 | break 486 | 487 | case '{': 488 | tokens.shift() 489 | value = onoptionMap(tokens) 490 | 491 | if (map[key] === undefined) map[key] = [] 492 | if (!Array.isArray(map[key])) throw new Error('Duplicate option map key ' + key) 493 | 494 | map[key].push(value) 495 | break 496 | 497 | default: 498 | throw new Error('Unexpected token in option map: ' + tokens[0]) 499 | } 500 | } 501 | 502 | throw new Error('No closing tag for option map') 503 | } 504 | 505 | var onimport = function (tokens) { 506 | tokens.shift() 507 | var file = tokens.shift().replace(/^"+|"+$/gm, '') 508 | 509 | if (tokens[0] !== ';') throw new Error('Unexpected token: ' + tokens[0] + '. Expected ";"') 510 | 511 | tokens.shift() 512 | return file 513 | } 514 | 515 | var onservice = function (tokens) { 516 | tokens.shift() 517 | 518 | var service = { 519 | name: tokens.shift(), 520 | methods: [], 521 | options: {} 522 | } 523 | 524 | if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0]) 525 | tokens.shift() 526 | 527 | while (tokens.length) { 528 | if (tokens[0] === '}') { 529 | tokens.shift() 530 | // there goes optional semicolon after the enclosing "}" 531 | if (tokens[0] === ';') tokens.shift() 532 | return service 533 | } 534 | 535 | switch (tokens[0]) { 536 | case 'option': 537 | var opt = onoption(tokens) 538 | if (service.options[opt.name] !== undefined) throw new Error('Duplicate option ' + opt.name) 539 | service.options[opt.name] = opt.value 540 | break 541 | case 'rpc': 542 | service.methods.push(onrpc(tokens)) 543 | break 544 | default: 545 | throw new Error('Unexpected token in service: ' + tokens[0]) 546 | } 547 | } 548 | 549 | throw new Error('No closing tag for service') 550 | } 551 | 552 | var onrpc = function (tokens) { 553 | tokens.shift() 554 | 555 | var rpc = { 556 | name: tokens.shift(), 557 | input_type: null, 558 | output_type: null, 559 | client_streaming: false, 560 | server_streaming: false, 561 | options: {} 562 | } 563 | 564 | if (tokens[0] !== '(') throw new Error('Expected ( but found ' + tokens[0]) 565 | tokens.shift() 566 | 567 | if (tokens[0] === 'stream') { 568 | tokens.shift() 569 | rpc.client_streaming = true 570 | } 571 | 572 | rpc.input_type = tokens.shift() 573 | 574 | if (tokens[0] !== ')') throw new Error('Expected ) but found ' + tokens[0]) 575 | tokens.shift() 576 | 577 | if (tokens[0] !== 'returns') throw new Error('Expected returns but found ' + tokens[0]) 578 | tokens.shift() 579 | 580 | if (tokens[0] !== '(') throw new Error('Expected ( but found ' + tokens[0]) 581 | tokens.shift() 582 | 583 | if (tokens[0] === 'stream') { 584 | tokens.shift() 585 | rpc.server_streaming = true 586 | } 587 | 588 | rpc.output_type = tokens.shift() 589 | 590 | if (tokens[0] !== ')') throw new Error('Expected ) but found ' + tokens[0]) 591 | tokens.shift() 592 | 593 | if (tokens[0] === ';') { 594 | tokens.shift() 595 | return rpc 596 | } 597 | 598 | if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0]) 599 | tokens.shift() 600 | 601 | while (tokens.length) { 602 | if (tokens[0] === '}') { 603 | tokens.shift() 604 | // there goes optional semicolon after the enclosing "}" 605 | if (tokens[0] === ';') tokens.shift() 606 | return rpc 607 | } 608 | 609 | if (tokens[0] === 'option') { 610 | var opt = onoption(tokens) 611 | if (rpc.options[opt.name] !== undefined) throw new Error('Duplicate option ' + opt.name) 612 | rpc.options[opt.name] = opt.value 613 | } else { 614 | throw new Error('Unexpected token in rpc options: ' + tokens[0]) 615 | } 616 | } 617 | 618 | throw new Error('No closing tag for rpc') 619 | } 620 | 621 | var parse = function (buf) { 622 | var tokens = tokenize(buf.toString()) 623 | // check for isolated strings in tokens by looking for opening quote 624 | for (var i = 0; i < tokens.length; i++) { 625 | if (/^("|')([^'"]*)$/.test(tokens[i])) { 626 | var j 627 | if (tokens[i].length === 1) { 628 | j = i + 1 629 | } else { 630 | j = i 631 | } 632 | // look ahead for the closing quote and collapse all 633 | // in-between tokens into a single token 634 | for (j; j < tokens.length; j++) { 635 | if (/^[^'"\\]*(?:\\.[^'"\\]*)*("|')$/.test(tokens[j])) { 636 | tokens = tokens.slice(0, i).concat(tokens.slice(i, j + 1).join('')).concat(tokens.slice(j + 1)) 637 | break 638 | } 639 | } 640 | } 641 | } 642 | var schema = { 643 | syntax: 3, 644 | package: null, 645 | imports: [], 646 | enums: [], 647 | messages: [], 648 | options: {}, 649 | extends: [] 650 | } 651 | 652 | var firstline = true 653 | 654 | while (tokens.length) { 655 | switch (tokens[0]) { 656 | case 'package': 657 | schema.package = onpackagename(tokens) 658 | break 659 | 660 | case 'syntax': 661 | if (!firstline) throw new Error('Protobuf syntax version should be first thing in file') 662 | schema.syntax = onsyntaxversion(tokens) 663 | break 664 | 665 | case 'message': 666 | schema.messages.push(onmessage(tokens)) 667 | break 668 | 669 | case 'enum': 670 | schema.enums.push(onenum(tokens)) 671 | break 672 | 673 | case 'option': 674 | var opt = onoption(tokens) 675 | if (schema.options[opt.name]) throw new Error('Duplicate option ' + opt.name) 676 | schema.options[opt.name] = opt.value 677 | break 678 | 679 | case 'import': 680 | schema.imports.push(onimport(tokens)) 681 | break 682 | 683 | case 'extend': 684 | schema.extends.push(onextend(tokens)) 685 | break 686 | 687 | case 'service': 688 | if (!schema.services) schema.services = [] 689 | schema.services.push(onservice(tokens)) 690 | break 691 | 692 | default: 693 | throw new Error('Unexpected token: ' + tokens[0]) 694 | } 695 | firstline = false 696 | } 697 | 698 | // now iterate over messages and propagate extends 699 | schema.extends.forEach(function (ext) { 700 | schema.messages.forEach(function (msg) { 701 | if (msg.name === ext.name) { 702 | ext.message.fields.forEach(function (field) { 703 | if (!msg.extensions || field.tag < msg.extensions.from || field.tag > msg.extensions.to) { 704 | throw new Error(msg.name + ' does not declare ' + field.tag + ' as an extension number') 705 | } 706 | msg.fields.push(field) 707 | }) 708 | } 709 | }) 710 | }) 711 | 712 | schema.messages.forEach(function (msg) { 713 | msg.fields.forEach(function (field) { 714 | var fieldSplit 715 | var messageName 716 | var nestedEnumName 717 | var message 718 | 719 | function enumNameIsFieldType (en) { 720 | return en.name === field.type 721 | } 722 | 723 | function enumNameIsNestedEnumName (en) { 724 | return en.name === nestedEnumName 725 | } 726 | 727 | if (field.options && field.options.packed === 'true') { 728 | if (PACKABLE_TYPES.indexOf(field.type) === -1) { 729 | // let's see if it's an enum 730 | if (field.type.indexOf('.') === -1) { 731 | if (msg.enums && msg.enums.some(enumNameIsFieldType)) { 732 | return 733 | } 734 | } else { 735 | fieldSplit = field.type.split('.') 736 | if (fieldSplit.length > 2) { 737 | throw new Error('what is this?') 738 | } 739 | 740 | messageName = fieldSplit[0] 741 | nestedEnumName = fieldSplit[1] 742 | 743 | schema.messages.some(function (msg) { 744 | if (msg.name === messageName) { 745 | message = msg 746 | return msg 747 | } 748 | }) 749 | 750 | if (message && message.enums && message.enums.some(enumNameIsNestedEnumName)) { 751 | return 752 | } 753 | } 754 | 755 | throw new Error( 756 | 'Fields of type ' + field.type + ' cannot be declared [packed=true]. ' + 757 | 'Only repeated fields of primitive numeric types (types which use ' + 758 | 'the varint, 32-bit, or 64-bit wire types) can be declared "packed". ' + 759 | 'See https://developers.google.com/protocol-buffers/docs/encoding#optional' 760 | ) 761 | } 762 | } 763 | }) 764 | }) 765 | 766 | return schema 767 | } 768 | 769 | module.exports = parse 770 | -------------------------------------------------------------------------------- /stringify.js: -------------------------------------------------------------------------------- 1 | var onimport = function (i, result) { 2 | result.push('import "' + i + '";', '') 3 | return result 4 | } 5 | 6 | var onfield = function (f, result) { 7 | var prefix = f.repeated ? 'repeated' : f.required ? 'required' : 'optional' 8 | if (f.type === 'map') prefix = 'map<' + f.map.from + ',' + f.map.to + '>' 9 | if (f.oneof) prefix = '' 10 | 11 | var opts = Object.keys(f.options || {}).map(function (key) { 12 | return key + ' = ' + f.options[key] 13 | }).join(',') 14 | 15 | if (opts) opts = ' [' + opts + ']' 16 | 17 | result.push((prefix ? prefix + ' ' : '') + (f.map === 'map' ? '' : f.type + ' ') + f.name + ' = ' + f.tag + opts + ';') 18 | return result 19 | } 20 | 21 | var onmessage = function (m, result) { 22 | result.push('message ' + m.name + ' {') 23 | 24 | if (!m.options) m.options = {} 25 | onoption(m.options, result) 26 | 27 | if (!m.enums) m.enums = [] 28 | m.enums.forEach(function (e) { 29 | result.push(onenum(e, [])) 30 | }) 31 | 32 | if (!m.messages) m.messages = [] 33 | m.messages.forEach(function (m) { 34 | result.push(onmessage(m, [])) 35 | }) 36 | 37 | var oneofs = {} 38 | 39 | if (!m.fields) m.fields = [] 40 | m.fields.forEach(function (f) { 41 | if (f.oneof) { 42 | if (!oneofs[f.oneof]) oneofs[f.oneof] = [] 43 | oneofs[f.oneof].push(onfield(f, [])) 44 | } else { 45 | result.push(onfield(f, [])) 46 | } 47 | }) 48 | 49 | Object.keys(oneofs).forEach(function (n) { 50 | oneofs[n].unshift('oneof ' + n + ' {') 51 | oneofs[n].push('}') 52 | result.push(oneofs[n]) 53 | }) 54 | 55 | result.push('}', '') 56 | return result 57 | } 58 | 59 | var onenum = function (e, result) { 60 | result.push('enum ' + e.name + ' {') 61 | if (!e.options) e.options = {} 62 | var options = onoption(e.options, []) 63 | if (options.length > 1) { 64 | result.push(options.slice(0, -1)) 65 | } 66 | Object.keys(e.values).map(function (v) { 67 | var val = onenumvalue(e.values[v]) 68 | result.push([v + ' = ' + val + ';']) 69 | }) 70 | result.push('}', '') 71 | return result 72 | } 73 | 74 | var onenumvalue = function (v, result) { 75 | var opts = Object.keys(v.options || {}).map(function (key) { 76 | return key + ' = ' + v.options[key] 77 | }).join(',') 78 | 79 | if (opts) opts = ' [' + opts + ']' 80 | var val = v.value + opts 81 | return val 82 | } 83 | 84 | var onoption = function (o, result) { 85 | var keys = Object.keys(o) 86 | keys.forEach(function (option) { 87 | var v = o[option] 88 | if (~option.indexOf('.')) option = '(' + option + ')' 89 | 90 | var type = typeof v 91 | 92 | if (type === 'object') { 93 | v = onoptionMap(v, []) 94 | if (v.length) result.push('option ' + option + ' = {', v, '};') 95 | } else { 96 | if (type === 'string' && option !== 'optimize_for') v = '"' + v + '"' 97 | result.push('option ' + option + ' = ' + v + ';') 98 | } 99 | }) 100 | if (keys.length > 0) { 101 | result.push('') 102 | } 103 | 104 | return result 105 | } 106 | 107 | var onoptionMap = function (o, result) { 108 | var keys = Object.keys(o) 109 | keys.forEach(function (k) { 110 | var v = o[k] 111 | 112 | var type = typeof v 113 | 114 | if (type === 'object') { 115 | if (Array.isArray(v)) { 116 | v.forEach(function (v) { 117 | v = onoptionMap(v, []) 118 | if (v.length) result.push(k + ' {', v, '}') 119 | }) 120 | } else { 121 | v = onoptionMap(v, []) 122 | if (v.length) result.push(k + ' {', v, '}') 123 | } 124 | } else { 125 | if (type === 'string') v = '"' + v + '"' 126 | result.push(k + ': ' + v) 127 | } 128 | }) 129 | 130 | return result 131 | } 132 | 133 | var onservices = function (s, result) { 134 | result.push('service ' + s.name + ' {') 135 | 136 | if (!s.options) s.options = {} 137 | onoption(s.options, result) 138 | if (!s.methods) s.methods = [] 139 | s.methods.forEach(function (m) { 140 | result.push(onrpc(m, [])) 141 | }) 142 | 143 | result.push('}', '') 144 | return result 145 | } 146 | 147 | var onrpc = function (rpc, result) { 148 | var def = 'rpc ' + rpc.name + '(' 149 | if (rpc.client_streaming) def += 'stream ' 150 | def += rpc.input_type + ') returns (' 151 | if (rpc.server_streaming) def += 'stream ' 152 | def += rpc.output_type + ')' 153 | 154 | if (!rpc.options) rpc.options = {} 155 | 156 | var options = onoption(rpc.options, []) 157 | if (options.length > 1) { 158 | result.push(def + ' {', options.slice(0, -1), '}') 159 | } else { 160 | result.push(def + ';') 161 | } 162 | 163 | return result 164 | } 165 | 166 | var indent = function (lvl) { 167 | return function (line) { 168 | if (Array.isArray(line)) return line.map(indent(lvl + ' ')).join('\n') 169 | return lvl + line 170 | } 171 | } 172 | 173 | module.exports = function (schema) { 174 | var result = [] 175 | 176 | result.push('syntax = "proto' + schema.syntax + '";', '') 177 | 178 | if (schema.package) result.push('package ' + schema.package + ';', '') 179 | 180 | if (schema.imports) { 181 | schema.imports.forEach(function (i) { 182 | onimport(i, result) 183 | }) 184 | } 185 | 186 | if (!schema.options) schema.options = {} 187 | 188 | onoption(schema.options, result) 189 | 190 | if (!schema.enums) schema.enums = [] 191 | schema.enums.forEach(function (e) { 192 | onenum(e, result) 193 | }) 194 | 195 | if (!schema.messages) schema.messages = [] 196 | schema.messages.forEach(function (m) { 197 | onmessage(m, result) 198 | }) 199 | 200 | if (schema.services) { 201 | schema.services.forEach(function (s) { 202 | onservices(s, result) 203 | }) 204 | } 205 | return result.map(indent('')).join('\n') 206 | } 207 | -------------------------------------------------------------------------------- /test/fixtures/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "Point", 10 | "enums": [], 11 | "extends": [], 12 | "messages": [], 13 | "options": {}, 14 | "extensions": null, 15 | "fields": [ 16 | { 17 | "name": "x", 18 | "type": "int32", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": true, 23 | "repeated": false, 24 | "options": {} 25 | }, 26 | { 27 | "name": "y", 28 | "type": "int32", 29 | "tag": 2, 30 | "map": null, 31 | "oneof": null, 32 | "required": true, 33 | "repeated": false, 34 | "options": {} 35 | }, 36 | { 37 | "name": "label", 38 | "type": "string", 39 | "tag": 3, 40 | "map": null, 41 | "oneof": null, 42 | "required": false, 43 | "repeated": false, 44 | "options": {} 45 | } 46 | ] 47 | }, 48 | { 49 | "name": "Line", 50 | "enums": [], 51 | "extends": [], 52 | "extensions": null, 53 | "messages": [], 54 | "options": {}, 55 | "fields": [ 56 | { 57 | "name": "start", 58 | "type": "Point", 59 | "tag": 1, 60 | "map": null, 61 | "oneof": null, 62 | "required": true, 63 | "repeated": false, 64 | "options": {} 65 | }, 66 | { 67 | "name": "end", 68 | "type": "Point", 69 | "tag": 2, 70 | "map": null, 71 | "oneof": null, 72 | "required": true, 73 | "repeated": false, 74 | "options": {} 75 | }, 76 | { 77 | "name": "label", 78 | "type": "string", 79 | "tag": 3, 80 | "map": null, 81 | "oneof": null, 82 | "required": false, 83 | "repeated": false, 84 | "options": {} 85 | } 86 | ] 87 | } 88 | ], 89 | "options":{} 90 | } 91 | -------------------------------------------------------------------------------- /test/fixtures/basic.proto: -------------------------------------------------------------------------------- 1 | message Point { 2 | required int32 x = 1; 3 | required int32 y = 2; 4 | optional string label = 3; 5 | } 6 | 7 | message Line { 8 | required Point start = 1; 9 | required Point end = 2; 10 | optional string label = 3; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "Point", 10 | "enums": [], 11 | "extends": [], 12 | "messages": [], 13 | "options": {}, 14 | "extensions": null, 15 | "fields": [ 16 | { 17 | "name": "x", 18 | "type": "int32", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": true, 23 | "repeated": false, 24 | "options": {} 25 | }, 26 | { 27 | "name": "y", 28 | "type": "int32", 29 | "tag": 2, 30 | "map": null, 31 | "oneof": null, 32 | "required": true, 33 | "repeated": false, 34 | "options": {} 35 | }, 36 | { 37 | "name": "label", 38 | "type": "string", 39 | "tag": 3, 40 | "map": null, 41 | "oneof": null, 42 | "required": false, 43 | "repeated": false, 44 | "options": {} 45 | } 46 | ] 47 | }, 48 | { 49 | "name": "Line", 50 | "enums": [], 51 | "extends": [], 52 | "options": {}, 53 | "extensions": null, 54 | "messages": [], 55 | "fields": [ 56 | { 57 | "name": "start", 58 | "type": "Point", 59 | "tag": 1, 60 | "map": null, 61 | "oneof": null, 62 | "required": true, 63 | "repeated": false, 64 | "options": {} 65 | }, 66 | { 67 | "name": "end", 68 | "type": "Point", 69 | "tag": 2, 70 | "map": null, 71 | "oneof": null, 72 | "required": true, 73 | "repeated": false, 74 | "options": {} 75 | }, 76 | { 77 | "name": "label", 78 | "type": "string", 79 | "tag": 3, 80 | "map": null, 81 | "oneof": null, 82 | "required": false, 83 | "repeated": false, 84 | "options": {} 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "A", 90 | "enums": [], 91 | "extends": [], 92 | "options": {}, 93 | "messages": [], 94 | "fields": [], 95 | "extensions": null 96 | } 97 | ], 98 | "options":{} 99 | } 100 | -------------------------------------------------------------------------------- /test/fixtures/comments.proto: -------------------------------------------------------------------------------- 1 | //Single-line comment 2 | message Point { 3 | required int32 x = 1; 4 | required int32 y = 2; 5 | optional string label = 3; 6 | } 7 | 8 | /*Multi-line comment*/ 9 | message Line {/* 10 | */required Point start = 1; 11 | required Point end = 2; 12 | optional string label = 3; 13 | } 14 | 15 | /** 16 | * Doxygen-style comment 17 | **/ 18 | message A { 19 | }//Comment after closing brace of last message 20 | -------------------------------------------------------------------------------- /test/fixtures/complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": "tutorial", 4 | "imports": [], 5 | "enums": [], 6 | "messages": [{ 7 | "name": "Person", 8 | "options": {}, 9 | "enums": [{ 10 | "name": "PhoneType", 11 | "values": { 12 | "MOBILE": { 13 | "value": 0, 14 | "options": { 15 | "some_enum_option": "true" 16 | } 17 | }, 18 | "HOME": { 19 | "value": 1, 20 | "options": {} 21 | }, 22 | "WORK": { 23 | "value": 2, 24 | "options": {} 25 | } 26 | }, 27 | "options": { 28 | "allow_alias": true, 29 | "custom_option": true 30 | } 31 | }], 32 | "extends": [], 33 | "messages": [{ 34 | "name": "PhoneNumber", 35 | "enums": [], 36 | "extends": [], 37 | "messages": [], 38 | "options": {}, 39 | "fields": [{ 40 | "name": "number", 41 | "type": "string", 42 | "tag": 1, 43 | "map": null, 44 | "oneof": null, 45 | "required": true, 46 | "repeated": false, 47 | "options": {} 48 | }, { 49 | "name": "type", 50 | "type": "PhoneType", 51 | "tag": 2, 52 | "map": null, 53 | "oneof": null, 54 | "required": false, 55 | "repeated": false, 56 | "options": { 57 | "default": "HOME" 58 | } 59 | }], 60 | "extensions": null 61 | }], 62 | "fields": [{ 63 | "name": "name", 64 | "type": "string", 65 | "tag": 1, 66 | "map": null, 67 | "oneof": null, 68 | "required": true, 69 | "repeated": false, 70 | "options": {} 71 | }, { 72 | "name": "id", 73 | "type": "int32", 74 | "tag": 2, 75 | "map": null, 76 | "oneof": null, 77 | "required": true, 78 | "repeated": false, 79 | "options": {} 80 | }, { 81 | "name": "email", 82 | "type": "string", 83 | "tag": 3, 84 | "map": null, 85 | "oneof": null, 86 | "required": false, 87 | "repeated": false, 88 | "options": {} 89 | }, { 90 | "name": "phone", 91 | "type": "PhoneNumber", 92 | "tag": 4, 93 | "map": null, 94 | "oneof": null, 95 | "required": false, 96 | "repeated": true, 97 | "options": {} 98 | }], 99 | "extensions": null 100 | }, { 101 | "name": "AddressBook", 102 | "enums": [], 103 | "extends": [], 104 | "messages": [], 105 | "options": {}, 106 | "fields": [{ 107 | "name": "person", 108 | "type": "Person", 109 | "tag": 1, 110 | "map": null, 111 | "oneof": null, 112 | "required": false, 113 | "repeated": true, 114 | "options": {} 115 | }], 116 | "extensions": null 117 | }], 118 | "options": { 119 | "java_package": "com.mafintosh.generated", 120 | "java_outer_classname": "Example", 121 | "java_generate_equals_and_hash": true, 122 | "optimize_for": "SPEED" 123 | }, 124 | "extends": [] 125 | } 126 | -------------------------------------------------------------------------------- /test/fixtures/complex.proto: -------------------------------------------------------------------------------- 1 | package tutorial; 2 | 3 | option java_package = "com.mafintosh.generated"; 4 | option java_outer_classname = "Example"; 5 | option java_generate_equals_and_hash = true; 6 | option optimize_for = SPEED; 7 | 8 | message Person { 9 | enum PhoneType { 10 | option allow_alias = true; 11 | option custom_option = true; 12 | MOBILE = 0 [some_enum_option = true]; 13 | HOME = 1; 14 | WORK = 2; 15 | } 16 | 17 | message PhoneNumber { 18 | required string number = 1; 19 | optional PhoneType type = 2 [default = HOME]; 20 | } 21 | 22 | required string name = 1; 23 | required int32 id = 2; 24 | optional string email = 3; 25 | repeated PhoneNumber phone = 4; 26 | } 27 | 28 | message AddressBook { 29 | repeated Person person = 1; 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [{ 7 | "name": "Person", 8 | "enums": [{ 9 | "name": "PhoneType", 10 | "values": { 11 | "MOBILE": { 12 | "value": 0, 13 | "options": { 14 | "enum_value_is_deprecated": "true", 15 | "some_second_option": "\"value1, value2, value3\"", 16 | "some_third_option": "'[value1, value2, value3]'" 17 | } 18 | }, 19 | "HOME": { 20 | "value": 1, 21 | "options": { 22 | "enum_value_is_deprecated": "true" 23 | } 24 | }, 25 | "WORK": { 26 | "value": 2, 27 | "options": {} 28 | } 29 | }, 30 | "options": { 31 | "allow_alias": true 32 | } 33 | }], 34 | "extends": [], 35 | "messages": [], 36 | "options": {}, 37 | "fields": [], 38 | "extensions": null 39 | }], 40 | "options": {}, 41 | "extends": [] 42 | } 43 | -------------------------------------------------------------------------------- /test/fixtures/enum.proto: -------------------------------------------------------------------------------- 1 | message Person { 2 | enum PhoneType { 3 | option allow_alias = true; 4 | MOBILE = 0 [(enum_value_is_deprecated) = true, (some_second_option) = "value1, value2, value3", (some_third_option) = '[value1, value2, value3]']; 5 | HOME = 1 [(enum_value_is_deprecated) = true]; 6 | WORK = 2; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/escaped-quotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [ 7 | { 8 | "name": "Event", 9 | "enums": [], 10 | "options": {}, 11 | "extends": [], 12 | "messages": [], 13 | "fields": [ 14 | { 15 | "name": "id", 16 | "type": "EntityId", 17 | "tag": 1, 18 | "map": null, 19 | "oneof": null, 20 | "required": false, 21 | "repeated": false, 22 | "options": { 23 | "tagger.tags": "\"a:'Hello, there', bson:\\\"_id,omitempty\\\"\"" 24 | } 25 | } 26 | ], 27 | "extensions": null 28 | } 29 | ], 30 | "options": {}, 31 | "extends": [] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/escaped-quotes.proto: -------------------------------------------------------------------------------- 1 | message Event { 2 | EntityId id = 1 [(tagger.tags) = "a:'Hello, there', bson:\"_id,omitempty\"" ]; 3 | } -------------------------------------------------------------------------------- /test/fixtures/extend.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [ 7 | { 8 | "name": "MsgNormal", 9 | "enums": [], 10 | "extends": [], 11 | "options": {}, 12 | "messages": [], 13 | "fields": [ 14 | { 15 | "name": "field1", 16 | "type": "int32", 17 | "tag": 1, 18 | "map": null, 19 | "oneof": null, 20 | "required": true, 21 | "repeated": false, 22 | "options": {} 23 | }, 24 | { 25 | "name": "field2", 26 | "type": "string", 27 | "tag": 2, 28 | "map": null, 29 | "oneof": null, 30 | "required": false, 31 | "repeated": false, 32 | "options": {} 33 | }, 34 | { 35 | "name": "field3", 36 | "type": "int32", 37 | "tag": 3, 38 | "map": null, 39 | "oneof": null, 40 | "required": true, 41 | "repeated": false, 42 | "options": {} 43 | }, 44 | { 45 | "name": "exField1", 46 | "type": "int32", 47 | "tag": 101, 48 | "map": null, 49 | "oneof": null, 50 | "required": false, 51 | "repeated": false, 52 | "options": {} 53 | }, 54 | { 55 | "name": "exField2", 56 | "type": "string", 57 | "tag": 102, 58 | "map": null, 59 | "oneof": null, 60 | "required": false, 61 | "repeated": false, 62 | "options": {} 63 | } 64 | ], 65 | "extensions": null 66 | }, 67 | { 68 | "name": "MsgExtend", 69 | "enums": [], 70 | "extends": [], 71 | "messages": [], 72 | "options": {}, 73 | "fields": [ 74 | { 75 | "name": "field1", 76 | "type": "int32", 77 | "tag": 1, 78 | "map": null, 79 | "oneof": null, 80 | "required": true, 81 | "repeated": false, 82 | "options": {} 83 | }, 84 | { 85 | "name": "field2", 86 | "type": "string", 87 | "tag": 2, 88 | "map": null, 89 | "oneof": null, 90 | "required": false, 91 | "repeated": false, 92 | "options": {} 93 | }, 94 | { 95 | "name": "field3", 96 | "type": "int32", 97 | "tag": 3, 98 | "map": null, 99 | "oneof": null, 100 | "required": true, 101 | "repeated": false, 102 | "options": {} 103 | }, 104 | { 105 | "name": "exField1", 106 | "type": "int32", 107 | "tag": 101, 108 | "map": null, 109 | "oneof": null, 110 | "required": false, 111 | "repeated": false, 112 | "options": {} 113 | }, 114 | { 115 | "name": "exField2", 116 | "type": "string", 117 | "tag": 102, 118 | "map": null, 119 | "oneof": null, 120 | "required": false, 121 | "repeated": false, 122 | "options": {} 123 | } 124 | ], 125 | "extensions": { 126 | "from": 100, 127 | "to": 200 128 | } 129 | } 130 | ], 131 | "options": {}, 132 | "extends": [ 133 | { 134 | "name": "MsgExtend", 135 | "message": { 136 | "name": "MsgExtend", 137 | "enums": [], 138 | "extends": [], 139 | "options": {}, 140 | "messages": [], 141 | "fields": [ 142 | { 143 | "name": "exField1", 144 | "type": "int32", 145 | "tag": 101, 146 | "map": null, 147 | "oneof": null, 148 | "required": false, 149 | "repeated": false, 150 | "options": {} 151 | }, 152 | { 153 | "name": "exField2", 154 | "type": "string", 155 | "tag": 102, 156 | "map": null, 157 | "oneof": null, 158 | "required": false, 159 | "repeated": false, 160 | "options": {} 161 | } 162 | ], 163 | "extensions": null 164 | } 165 | } 166 | ] 167 | } 168 | -------------------------------------------------------------------------------- /test/fixtures/extend.proto: -------------------------------------------------------------------------------- 1 | message MsgNormal { 2 | required int32 field1 = 1; 3 | optional string field2 = 2; 4 | required int32 field3 = 3; 5 | optional int32 exField1 = 101; 6 | optional string exField2 = 102; 7 | } 8 | 9 | message MsgExtend { 10 | required int32 field1 = 1; 11 | optional string field2 = 2; 12 | required int32 field3 = 3; 13 | extensions 100 to 200; 14 | } 15 | 16 | extend MsgExtend { 17 | optional int32 exField1 = 101; 18 | optional string exField2 = 102; 19 | } -------------------------------------------------------------------------------- /test/fixtures/import.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": ["./result.proto", "./other_result.proto"], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "SearchResponse", 10 | "extensions": null, 11 | "enums": [], 12 | "extends": [], 13 | "options": {}, 14 | "messages": [], 15 | "fields": [ 16 | { 17 | "name": "result", 18 | "type": "Result", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": false, 23 | "repeated": true, 24 | "options": {} 25 | } 26 | ] 27 | } 28 | ], 29 | "options": {} 30 | } -------------------------------------------------------------------------------- /test/fixtures/import.proto: -------------------------------------------------------------------------------- 1 | import "./result.proto"; 2 | import "./other_result.proto"; 3 | 4 | message SearchResponse { 5 | repeated Result result = 1; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/map.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [ 7 | { 8 | "name": "Data", 9 | "enums": [], 10 | "options": {}, 11 | "extends": [], 12 | "messages": [], 13 | "fields": [ 14 | { 15 | "name": "data", 16 | "type": "map", 17 | "map": { 18 | "from": "string", 19 | "to": "bytes" 20 | }, 21 | "oneof": null, 22 | "tag": 1, 23 | "required": false, 24 | "repeated": false, 25 | "options": {} 26 | } 27 | ], 28 | "extensions": null 29 | } 30 | ], 31 | "options": {}, 32 | "extends": [] 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/map.proto: -------------------------------------------------------------------------------- 1 | message Data { 2 | map data = 1; 3 | } -------------------------------------------------------------------------------- /test/fixtures/no-tags.proto: -------------------------------------------------------------------------------- 1 | message wallet { 2 | optional int32 dollars; 3 | optional int32 pesos; 4 | } 5 | 6 | message traveller { 7 | required wallet wallet; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/oneof.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [ 7 | { 8 | "name": "SampleMessage", 9 | "enums": [], 10 | "extends": [], 11 | "messages": [], 12 | "options": {}, 13 | "fields": [ 14 | { 15 | "name": "name", 16 | "type": "string", 17 | "tag": 4, 18 | "map": null, 19 | "oneof": "test_oneof", 20 | "required": false, 21 | "repeated": false, 22 | "options": {} 23 | }, 24 | { 25 | "name": "sub_message", 26 | "type": "SubMessage", 27 | "tag": 9, 28 | "map": null, 29 | "oneof": "test_oneof", 30 | "required": false, 31 | "repeated": false, 32 | "options": {} 33 | } 34 | 35 | ], 36 | "extensions": null 37 | } 38 | ], 39 | "options": {}, 40 | "extends": [] 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/oneof.proto: -------------------------------------------------------------------------------- 1 | message SampleMessage { 2 | oneof test_oneof { 3 | string name = 4; 4 | SubMessage sub_message = 9; 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/option.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [ 5 | "google/protobuf/descriptor.proto" 6 | ], 7 | "enums": [ 8 | { 9 | "name": "MyEnum", 10 | "values": { 11 | "FOO": { 12 | "value": 1, 13 | "options": { 14 | "my_enum_value_option": "321" 15 | } 16 | }, 17 | "BAR": { 18 | "value": 2, 19 | "options": {} 20 | } 21 | }, 22 | "options": { 23 | "my_enum_option": true 24 | } 25 | } 26 | ], 27 | "messages": [ 28 | { 29 | "name": "MyMessage", 30 | "enums": [], 31 | "extends": [], 32 | "messages": [], 33 | "options": { 34 | "my_message_option": "1234" 35 | }, 36 | "fields": [ 37 | { 38 | "name": "foo", 39 | "type": "int32", 40 | "tag": 1, 41 | "map": null, 42 | "oneof": null, 43 | "required": false, 44 | "repeated": false, 45 | "options": { 46 | "my_field_option": "4.5" 47 | } 48 | }, 49 | { 50 | "name": "bar", 51 | "type": "string", 52 | "tag": 2, 53 | "map": null, 54 | "oneof": null, 55 | "required": false, 56 | "repeated": false, 57 | "options": {} 58 | } 59 | ], 60 | "extensions": null 61 | }, 62 | { 63 | "name": "RequestType", 64 | "enums": [], 65 | "extends": [], 66 | "messages": [], 67 | "options": {}, 68 | "fields": [], 69 | "extensions": null 70 | }, 71 | { 72 | "name": "ResponseType", 73 | "enums": [], 74 | "extends": [], 75 | "options": {}, 76 | "messages": [], 77 | "fields": [], 78 | "extensions": null 79 | }, 80 | { 81 | "name": "FooOptions", 82 | "enums": [], 83 | "extends": [], 84 | "options": {}, 85 | "messages": [], 86 | "fields": [ 87 | { 88 | "name": "opt1", 89 | "type": "int32", 90 | "tag": 1, 91 | "map": null, 92 | "oneof": null, 93 | "required": false, 94 | "repeated": false, 95 | "options": {} 96 | }, 97 | { 98 | "name": "opt2", 99 | "type": "string", 100 | "tag": 2, 101 | "map": null, 102 | "oneof": null, 103 | "required": false, 104 | "repeated": false, 105 | "options": {} 106 | } 107 | ], 108 | "extensions": null 109 | }, 110 | { 111 | "name": "Bar", 112 | "enums": [], 113 | "extends": [], 114 | "options": {}, 115 | "messages": [], 116 | "fields": [ 117 | { 118 | "name": "a", 119 | "type": "int32", 120 | "tag": 1, 121 | "map": null, 122 | "oneof": null, 123 | "required": false, 124 | "repeated": false, 125 | "options": { 126 | "foo_options": { 127 | "opt1": "123", 128 | "opt2": "\"baz\"" 129 | } 130 | } 131 | }, 132 | { 133 | "name": "b", 134 | "type": "int32", 135 | "tag": 2, 136 | "map": null, 137 | "oneof": null, 138 | "required": false, 139 | "repeated": false, 140 | "options": { 141 | "foo_options": { 142 | "opt1": "123", 143 | "opt2": "\"baz\"" 144 | } 145 | } 146 | } 147 | ], 148 | "extensions": null 149 | } 150 | ], 151 | "options": { 152 | "my_file_option": "Hello world!" 153 | }, 154 | "extends": [ 155 | { 156 | "name": "google.protobuf.FileOptions", 157 | "message": { 158 | "name": "google.protobuf.FileOptions", 159 | "enums": [], 160 | "extends": [], 161 | "messages": [], 162 | "options": {}, 163 | "fields": [ 164 | { 165 | "name": "my_file_option", 166 | "type": "string", 167 | "tag": 50000, 168 | "map": null, 169 | "oneof": null, 170 | "required": false, 171 | "repeated": false, 172 | "options": {} 173 | } 174 | ], 175 | "extensions": null 176 | } 177 | }, 178 | { 179 | "name": "google.protobuf.MessageOptions", 180 | "message": { 181 | "name": "google.protobuf.MessageOptions", 182 | "enums": [], 183 | "extends": [], 184 | "messages": [], 185 | "options": {}, 186 | "fields": [ 187 | { 188 | "name": "my_message_option", 189 | "type": "int32", 190 | "tag": 50001, 191 | "map": null, 192 | "oneof": null, 193 | "required": false, 194 | "repeated": false, 195 | "options": {} 196 | } 197 | ], 198 | "extensions": null 199 | } 200 | }, 201 | { 202 | "name": "google.protobuf.FieldOptions", 203 | "message": { 204 | "name": "google.protobuf.FieldOptions", 205 | "enums": [], 206 | "extends": [], 207 | "messages": [], 208 | "options": {}, 209 | "fields": [ 210 | { 211 | "name": "my_field_option", 212 | "type": "float", 213 | "tag": 50002, 214 | "map": null, 215 | "oneof": null, 216 | "required": false, 217 | "repeated": false, 218 | "options": {} 219 | } 220 | ], 221 | "extensions": null 222 | } 223 | }, 224 | { 225 | "name": "google.protobuf.EnumOptions", 226 | "message": { 227 | "name": "google.protobuf.EnumOptions", 228 | "enums": [], 229 | "extends": [], 230 | "messages": [], 231 | "options": {}, 232 | "fields": [ 233 | { 234 | "name": "my_enum_option", 235 | "type": "bool", 236 | "tag": 50003, 237 | "map": null, 238 | "oneof": null, 239 | "required": false, 240 | "repeated": false, 241 | "options": {} 242 | } 243 | ], 244 | "extensions": null 245 | } 246 | }, 247 | { 248 | "name": "google.protobuf.EnumValueOptions", 249 | "message": { 250 | "name": "google.protobuf.EnumValueOptions", 251 | "enums": [], 252 | "extends": [], 253 | "messages": [], 254 | "options": {}, 255 | "fields": [ 256 | { 257 | "name": "my_enum_value_option", 258 | "type": "uint32", 259 | "tag": 50004, 260 | "map": null, 261 | "oneof": null, 262 | "required": false, 263 | "repeated": false, 264 | "options": {} 265 | } 266 | ], 267 | "extensions": null 268 | } 269 | }, 270 | { 271 | "name": "google.protobuf.ServiceOptions", 272 | "message": { 273 | "name": "google.protobuf.ServiceOptions", 274 | "enums": [], 275 | "extends": [], 276 | "messages": [], 277 | "options": {}, 278 | "fields": [ 279 | { 280 | "name": "my_service_option", 281 | "type": "MyEnum", 282 | "tag": 50005, 283 | "map": null, 284 | "oneof": null, 285 | "required": false, 286 | "repeated": false, 287 | "options": {} 288 | } 289 | ], 290 | "extensions": null 291 | } 292 | }, 293 | { 294 | "name": "google.protobuf.MethodOptions", 295 | "message": { 296 | "name": "google.protobuf.MethodOptions", 297 | "enums": [], 298 | "extends": [], 299 | "messages": [], 300 | "options": {}, 301 | "fields": [ 302 | { 303 | "name": "my_method_option", 304 | "type": "MyMessage", 305 | "tag": 50006, 306 | "map": null, 307 | "oneof": null, 308 | "required": false, 309 | "repeated": false, 310 | "options": {} 311 | } 312 | ], 313 | "extensions": null 314 | } 315 | }, 316 | { 317 | "name": "google.protobuf.FieldOptions", 318 | "message": { 319 | "name": "google.protobuf.FieldOptions", 320 | "enums": [], 321 | "extends": [], 322 | "messages": [], 323 | "options": {}, 324 | "fields": [ 325 | { 326 | "name": "foo_options", 327 | "type": "FooOptions", 328 | "tag": 1234, 329 | "map": null, 330 | "oneof": null, 331 | "required": false, 332 | "repeated": false, 333 | "options": {} 334 | } 335 | ], 336 | "extensions": null 337 | } 338 | } 339 | ], 340 | "services": [ 341 | { 342 | "name": "MyService", 343 | "methods": [ 344 | { 345 | "name": "MyMethod", 346 | "input_type": "RequestType", 347 | "output_type": "ResponseType", 348 | "client_streaming": false, 349 | "server_streaming": false, 350 | "options": { 351 | "my_method_option.foo": "567", 352 | "my_method_option.bar": "Some string" 353 | } 354 | } 355 | ], 356 | "options": { 357 | "my_service_option": "FOO", 358 | "my_service_option_map": { 359 | "foo": "bar" 360 | } 361 | } 362 | } 363 | ] 364 | } 365 | -------------------------------------------------------------------------------- /test/fixtures/option.proto: -------------------------------------------------------------------------------- 1 | import "google/protobuf/descriptor.proto"; 2 | 3 | extend google.protobuf.FileOptions { 4 | optional string my_file_option = 50000; 5 | } 6 | extend google.protobuf.MessageOptions { 7 | optional int32 my_message_option = 50001; 8 | } 9 | extend google.protobuf.FieldOptions { 10 | optional float my_field_option = 50002; 11 | } 12 | extend google.protobuf.EnumOptions { 13 | optional bool my_enum_option = 50003; 14 | } 15 | extend google.protobuf.EnumValueOptions { 16 | optional uint32 my_enum_value_option = 50004; 17 | } 18 | extend google.protobuf.ServiceOptions { 19 | optional MyEnum my_service_option = 50005; 20 | } 21 | extend google.protobuf.MethodOptions { 22 | optional MyMessage my_method_option = 50006; 23 | } 24 | 25 | option (my_file_option) = "Hello world!"; 26 | 27 | message MyMessage { 28 | option (my_message_option) = 1234; 29 | 30 | optional int32 foo = 1 [(my_field_option) = 4.5]; 31 | optional string bar = 2; 32 | } 33 | 34 | enum MyEnum { 35 | option (my_enum_option) = true; 36 | 37 | FOO = 1 [(my_enum_value_option) = 321]; 38 | BAR = 2; 39 | } 40 | 41 | message RequestType {} 42 | message ResponseType {} 43 | 44 | service MyService { 45 | option (my_service_option) = FOO; 46 | option (my_service_option_map) = { 47 | foo: "bar"; 48 | }; 49 | 50 | rpc MyMethod(RequestType) returns(ResponseType) { 51 | // Note: my_method_option has type MyMessage. We can set each field 52 | // within it using a separate "option" line. 53 | option (my_method_option).foo = 567; 54 | option (my_method_option).bar = "Some string"; 55 | } 56 | } 57 | 58 | message FooOptions { 59 | optional int32 opt1 = 1; 60 | optional string opt2 = 2; 61 | } 62 | 63 | extend google.protobuf.FieldOptions { 64 | optional FooOptions foo_options = 1234; 65 | } 66 | 67 | // usage: 68 | message Bar { 69 | optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"]; 70 | // alternative aggregate syntax (uses TextFormat): 71 | optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }]; 72 | } 73 | -------------------------------------------------------------------------------- /test/fixtures/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "messages": [{ 7 | "name": "OptionFields", 8 | "enums": [], 9 | "extends": [], 10 | "messages": [], 11 | "options": {}, 12 | "fields": [{ 13 | "name": "type", 14 | "type": "string", 15 | "tag": 2, 16 | "map": null, 17 | "oneof": null, 18 | "required": false, 19 | "repeated": false, 20 | "options": { 21 | "mylist": "\"some,values,[here]\"" 22 | } 23 | }], 24 | "extensions": null 25 | }, { 26 | "name": "MoreOptionFields", 27 | "enums": [], 28 | "extends": [], 29 | "messages": [], 30 | "options": {}, 31 | "fields": [{ 32 | "name": "values", 33 | "type": "string", 34 | "tag": 3, 35 | "map": null, 36 | "oneof": null, 37 | "required": false, 38 | "repeated": false, 39 | "options": { 40 | "mylist2": "'[more, values], [here]'" 41 | } 42 | }], 43 | "extensions": null 44 | }], 45 | "options": {}, 46 | "extends": [] 47 | } 48 | -------------------------------------------------------------------------------- /test/fixtures/options.proto: -------------------------------------------------------------------------------- 1 | 2 | message OptionFields { 3 | optional string type = 2 [mylist = "some,values,[here]"]; 4 | } 5 | 6 | message MoreOptionFields { 7 | optional string values = 3 [mylist2 = '[more, values], [here]']; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/pheromon-trajectories.proto: -------------------------------------------------------------------------------- 1 | message deviceTrajectory { 2 | required bytes dates = 1 [packed=true]; 3 | required bytes signal_strengths = 2 [packed=true]; 4 | } 5 | 6 | message trajectories { 7 | repeated deviceTrajectory trajectories = 1 [packed=true]; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/reserved.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [ 6 | { 7 | "name": "ReservedEnum", 8 | "values": { 9 | "x": { 10 | "value": 1, 11 | "options": [] 12 | }, 13 | "y": { 14 | "value": 10, 15 | "options": [] 16 | } 17 | }, 18 | "options": {} 19 | } 20 | ], 21 | "messages": [ 22 | { 23 | "name": "Reserved", 24 | "extensions": null, 25 | "enums": [], 26 | "extends": [], 27 | "messages": [], 28 | "options": {}, 29 | "fields": [ 30 | { 31 | "name": "x", 32 | "type": "string", 33 | "tag": 1, 34 | "map": null, 35 | "oneof": null, 36 | "required": false, 37 | "repeated": false, 38 | "options": {} 39 | }, 40 | { 41 | "name": "y", 42 | "type": "string", 43 | "tag": 10, 44 | "map": null, 45 | "oneof": null, 46 | "required": false, 47 | "repeated": false, 48 | "options": {} 49 | } 50 | ] 51 | } 52 | ], 53 | "options": {}, 54 | "extends": [] 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/reserved.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Reserved { 4 | string x = 1; 5 | reserved 2, 3; 6 | reserved 5 to 9; 7 | reserved "foo", "bar"; 8 | string y = 10; 9 | } 10 | 11 | enum ReservedEnum { 12 | x = 1; 13 | reserved 2, 3; 14 | reserved 5 to 9; 15 | reserved "foo", "bar"; 16 | y = 10; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": ["./result.proto"], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "SearchResponse", 10 | "extensions": null, 11 | "enums": [], 12 | "extends": [], 13 | "options": {}, 14 | "messages": [], 15 | "fields": [ 16 | { 17 | "name": "result", 18 | "type": "Result", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": false, 23 | "repeated": true, 24 | "options": {} 25 | } 26 | ] 27 | } 28 | ], 29 | "options":{} 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/search.proto: -------------------------------------------------------------------------------- 1 | import "./result.proto"; 2 | 3 | message SearchResponse { 4 | repeated Result result = 1; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 3, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "HelloRequest", 10 | "extensions": null, 11 | "enums": [], 12 | "extends": [], 13 | "options": {}, 14 | "messages": [], 15 | "fields": [ 16 | { 17 | "name": "greeting", 18 | "type": "string", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": false, 23 | "repeated": false, 24 | "options": {} 25 | } 26 | ] 27 | }, 28 | { 29 | "name": "HelloResponse", 30 | "extensions": null, 31 | "enums": [], 32 | "extends": [], 33 | "messages": [], 34 | "options": {}, 35 | "fields": [ 36 | { 37 | "name": "reply", 38 | "type": "string", 39 | "tag": 1, 40 | "map": null, 41 | "oneof": null, 42 | "required": true, 43 | "repeated": false, 44 | "options": {} 45 | } 46 | ] 47 | } 48 | ], 49 | "services": [ 50 | { 51 | "name": "HelloService", 52 | "methods": [ 53 | { 54 | "name": "SayHello", 55 | "input_type": "HelloRequest", 56 | "output_type": "HelloResponse", 57 | "client_streaming": false, 58 | "server_streaming": false, 59 | "options": { 60 | "google.api.http": { 61 | "get": "/v1/say-hello/echo/{greeting}", 62 | "additional_bindings": [ 63 | { 64 | "post": "/v2/say-hello", 65 | "body": "greeting" 66 | }, 67 | { 68 | "get": "/v2/say-hello" 69 | } 70 | ] 71 | } 72 | } 73 | }, 74 | { 75 | "name": "LotsOfReplies", 76 | "input_type": "HelloRequest", 77 | "output_type": "HelloResponse", 78 | "client_streaming": false, 79 | "server_streaming": true, 80 | "options": {} 81 | }, 82 | { 83 | "name": "LotsOfGreetings", 84 | "input_type": "HelloRequest", 85 | "output_type": "HelloResponse", 86 | "client_streaming": true, 87 | "server_streaming": false, 88 | "options": { 89 | "google.api.http": { 90 | "post": "/v1/lots-of-greetings", 91 | "body": "*" 92 | } 93 | } 94 | }, 95 | { 96 | "name": "BidiHello", 97 | "input_type": "HelloRequest", 98 | "output_type": "HelloResponse", 99 | "client_streaming": true, 100 | "server_streaming": true, 101 | "options": {} 102 | } 103 | ], 104 | "options": {} 105 | } 106 | ], 107 | "options": {} 108 | } 109 | -------------------------------------------------------------------------------- /test/fixtures/service.proto: -------------------------------------------------------------------------------- 1 | message HelloRequest { 2 | optional string greeting = 1; 3 | } 4 | 5 | message HelloResponse { 6 | required string reply = 1; 7 | } 8 | 9 | service HelloService { 10 | rpc SayHello(HelloRequest) returns (HelloResponse) { 11 | option (google.api.http) = { 12 | get: "/v1/say-hello/echo/{greeting}" 13 | additional_bindings { 14 | post: "/v2/say-hello" 15 | body: "greeting" 16 | } 17 | additional_bindings { 18 | get: "/v2/say-hello" 19 | } 20 | }; 21 | } 22 | rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); 23 | rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { 24 | option (google.api.http) = { 25 | post: "/v1/lots-of-greetings" 26 | body: "*" 27 | }; 28 | } 29 | rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/valid-packed.proto: -------------------------------------------------------------------------------- 1 | message EnumCarrying{ 2 | enum E{ 3 | A = 0; 4 | B = 1; 5 | } 6 | 7 | } 8 | 9 | message ValidPacked { 10 | // varint wire types 11 | repeated int32 f1 = 1 [packed = true]; 12 | repeated int64 f2 = 2 [packed = true]; 13 | repeated uint32 f3 = 3 [packed = true]; 14 | repeated uint64 f4 = 4 [packed = true]; 15 | repeated sint32 f5 = 5 [packed = true]; 16 | repeated sint64 f6 = 6 [packed = true]; 17 | repeated bool f7 = 7 [packed = true]; 18 | enum Corpus { 19 | UNIVERSAL = 0; 20 | WEB = 1; 21 | } 22 | repeated Corpus f8 = 8 [packed = true]; 23 | repeated EnumCarrying.E f9 = 9 [packed = true]; 24 | 25 | // 64-bit wire types 26 | repeated fixed64 f10 = 10 [packed = true]; 27 | repeated sfixed64 f11 = 11 [packed = true]; 28 | repeated double f12 = 12 [packed = true]; 29 | 30 | // 32-bit wire types 31 | repeated fixed32 f13 = 13 [packed = true]; 32 | repeated sfixed32 f14 = 14 [packed = true]; 33 | repeated float f15 = 15 [packed = true]; 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": 2, 3 | "package": null, 4 | "imports": [], 5 | "enums": [], 6 | "extends": [], 7 | "messages": [ 8 | { 9 | "name": "Point", 10 | "enums": [], 11 | "extends": [], 12 | "messages": [], 13 | "extensions": null, 14 | "options": {}, 15 | "fields": [ 16 | { 17 | "name": "x", 18 | "type": "int32", 19 | "tag": 1, 20 | "map": null, 21 | "oneof": null, 22 | "required": true, 23 | "repeated": false, 24 | "options": {} 25 | }, 26 | { 27 | "name": "y", 28 | "type": "int32", 29 | "tag": 2, 30 | "map": null, 31 | "oneof": null, 32 | "required": true, 33 | "repeated": false, 34 | "options": {} 35 | }, 36 | { 37 | "name": "label", 38 | "type": "string", 39 | "tag": 3, 40 | "map": null, 41 | "oneof": null, 42 | "required": false, 43 | "repeated": false, 44 | "options": {} 45 | } 46 | ] 47 | }, 48 | { 49 | "name": "Line", 50 | "enums": [], 51 | "extends": [], 52 | "extensions": null, 53 | "options": {}, 54 | "messages": [], 55 | "fields": [ 56 | { 57 | "name": "start", 58 | "type": "Point", 59 | "tag": 1, 60 | "map": null, 61 | "oneof": null, 62 | "required": true, 63 | "repeated": false, 64 | "options": {} 65 | }, 66 | { 67 | "name": "end", 68 | "type": "Point", 69 | "tag": 2, 70 | "map": null, 71 | "oneof": null, 72 | "required": true, 73 | "repeated": false, 74 | "options": {} 75 | }, 76 | { 77 | "name": "label", 78 | "type": "string", 79 | "tag": 3, 80 | "map": null, 81 | "oneof": null, 82 | "required": false, 83 | "repeated": false, 84 | "options": {} 85 | } 86 | ] 87 | } 88 | ], 89 | "options":{} 90 | } 91 | -------------------------------------------------------------------------------- /test/fixtures/version.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Point { 4 | required int32 x = 1; 5 | required int32 y = 2; 6 | optional string label = 3; 7 | } 8 | 9 | message Line { 10 | required Point start = 1; 11 | required Point end = 2; 12 | optional string label = 3; 13 | } 14 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var path = require('path') 3 | var fs = require('fs') 4 | var schema = require('../') 5 | 6 | var fixture = function (name) { 7 | return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf-8') 8 | } 9 | 10 | tape('basic parse', function (t) { 11 | t.same(schema.parse(fixture('basic.proto')), require('./fixtures/basic.json')) 12 | t.end() 13 | }) 14 | 15 | tape('basic parse + stringify', function (t) { 16 | var syntax = 'syntax = "proto3";\n\n' 17 | t.same(schema.stringify(schema.parse(fixture('basic.proto'))), syntax + fixture('basic.proto')) 18 | t.end() 19 | }) 20 | 21 | tape('complex parse', function (t) { 22 | t.same(schema.parse(fixture('complex.proto')), require('./fixtures/complex.json')) 23 | t.end() 24 | }) 25 | 26 | tape('complex parse + stringify', function (t) { 27 | var syntax = 'syntax = "proto3";\n\n' 28 | t.same(schema.stringify(schema.parse(fixture('complex.proto'))), syntax + fixture('complex.proto')) 29 | t.end() 30 | }) 31 | 32 | tape('throws on invalid', function (t) { 33 | t.plan(2) 34 | try { 35 | schema.parse('hello world') 36 | } catch (err) { 37 | t.ok(true, 'should fail') 38 | } 39 | try { 40 | schema.parse('message Foo { lol }') 41 | } catch (err) { 42 | t.ok(true, 'should fail') 43 | } 44 | }) 45 | 46 | tape('comments parse', function (t) { 47 | t.same(schema.parse(fixture('comments.proto')), require('./fixtures/comments.json')) 48 | t.end() 49 | }) 50 | 51 | tape('schema with imports', function (t) { 52 | t.same(schema.parse(fixture('search.proto')), require('./fixtures/search.json')) 53 | t.end() 54 | }) 55 | 56 | tape('schema with imports loaded by path', function (t) { 57 | t.same(schema.parse(fixture('search.proto')), require('./fixtures/search.json')) 58 | t.end() 59 | }) 60 | 61 | tape('schema with extends', function (t) { 62 | t.same(schema.parse(fixture('extend.proto')), require('./fixtures/extend.json')) 63 | t.end() 64 | }) 65 | 66 | tape('comparing extended and not extended schema', function (t) { 67 | var sch = schema.parse(fixture('extend.proto')) 68 | t.same(sch.messages.MsgNormal, sch.messages.MsgExtend) 69 | t.end() 70 | }) 71 | 72 | tape('schema with oneof', function (t) { 73 | t.same(schema.parse(fixture('oneof.proto')), require('./fixtures/oneof.json')) 74 | t.end() 75 | }) 76 | 77 | tape('schema with map', function (t) { 78 | t.same(schema.parse(fixture('map.proto')), require('./fixtures/map.json')) 79 | t.end() 80 | }) 81 | 82 | tape('schema with syntax version', function (t) { 83 | t.same(schema.parse(fixture('version.proto')), require('./fixtures/version.json')) 84 | t.end() 85 | }) 86 | 87 | tape('throws on misplaced syntax version', function (t) { 88 | t.plan(1) 89 | try { 90 | schema.parse('message Foo { required int32 a = 1; }\n syntax = "proto3"') 91 | } catch (err) { 92 | t.ok(true, 'should fail') 93 | } 94 | }) 95 | 96 | tape('schema with reserved characters in options', function (t) { 97 | t.same(schema.parse(fixture('options.proto')), require('./fixtures/options.json')) 98 | t.end() 99 | }) 100 | 101 | tape('service parse', function (t) { 102 | t.same(schema.parse(fixture('service.proto')), require('./fixtures/service.json')) 103 | t.end() 104 | }) 105 | 106 | tape('service parse + stringify', function (t) { 107 | var syntax = 'syntax = "proto3";\n\n' 108 | t.same(schema.stringify(schema.parse(fixture('service.proto'))), syntax + fixture('service.proto')) 109 | t.end() 110 | }) 111 | 112 | tape('import parse + stringify', function (t) { 113 | var syntax = 'syntax = "proto3";\n\n' 114 | t.same(schema.stringify(schema.parse(fixture('search.proto'))), syntax + fixture('search.proto')) 115 | t.end() 116 | }) 117 | 118 | tape('enums with options', function (t) { 119 | t.same(schema.parse(fixture('enum.proto')), require('./fixtures/enum.json')) 120 | t.end() 121 | }) 122 | 123 | tape('fail on no tags', function (t) { 124 | t.throws(function () { 125 | schema.parse(fixture('no-tags.proto')) 126 | }) 127 | t.end() 128 | }) 129 | 130 | tape('reserved', function (t) { 131 | t.same(schema.parse(fixture('reserved.proto')), require('./fixtures/reserved.json')) 132 | t.end() 133 | }) 134 | 135 | tape('varint, 64-bit and 32-bit wire types can be packed', function (t) { 136 | t.doesNotThrow(function () { 137 | schema.parse(fixture('valid-packed.proto')) 138 | }, 'should not throw') 139 | t.end() 140 | }) 141 | 142 | tape('non-primitive packed should throw', function (t) { 143 | t.throws(function () { 144 | schema.parse(fixture('pheromon-trajectories.proto')) 145 | }, 'should throw') 146 | t.end() 147 | }) 148 | 149 | tape('custom options parse', function (t) { 150 | t.same(schema.parse(fixture('option.proto')), require('./fixtures/option.json')) 151 | t.end() 152 | }) 153 | 154 | tape('escaped quotes in option value parse', function (t) { 155 | t.same(schema.parse(fixture('escaped-quotes.proto')), require('./fixtures/escaped-quotes.json')) 156 | t.end() 157 | }) 158 | -------------------------------------------------------------------------------- /tokenize.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sch) { 2 | var noComments = function (line) { 3 | var i = line.indexOf('//') 4 | return i > -1 ? line.slice(0, i) : line 5 | } 6 | 7 | var noMultilineComments = function () { 8 | var inside = false 9 | return function (token) { 10 | if (token === '/*') { 11 | inside = true 12 | return false 13 | } 14 | if (token === '*/') { 15 | inside = false 16 | return false 17 | } 18 | return !inside 19 | } 20 | } 21 | 22 | var trim = function (line) { 23 | return line.trim() 24 | } 25 | 26 | var removeQuotedLines = function (list) { 27 | return function (str) { 28 | var s = '$' + list.length + '$' 29 | list.push(str) 30 | return s 31 | } 32 | } 33 | 34 | var restoreQuotedLines = function (list) { 35 | var re = /^\$(\d+)\$$/ 36 | return function (line) { 37 | var m = line.match(re) 38 | return m ? list[+m[1]] : line 39 | } 40 | } 41 | 42 | var replacements = [] 43 | return sch 44 | .replace(/"(\\"|[^"\n])*?"|'(\\'|[^'\n])*?'/gm, removeQuotedLines(replacements)) 45 | .replace(/([;,{}()=:[\]<>]|\/\*|\*\/)/g, ' $1 ') 46 | .split(/\n/) 47 | .map(trim) 48 | .filter(Boolean) 49 | .map(noComments) 50 | .map(trim) 51 | .filter(Boolean) 52 | .join('\n') 53 | .split(/\s+|\n+/gm) 54 | .filter(noMultilineComments()) 55 | .map(restoreQuotedLines(replacements)) 56 | } 57 | --------------------------------------------------------------------------------