├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── datatype.go ├── datatype_test.go ├── doc.go ├── elements.go ├── example_parse_test.go ├── example_parsefile_test.go ├── examples ├── internal │ └── numbers.proto └── mathservice.proto ├── examples_validator_test.go ├── go.mod ├── import_module_provider.go ├── parse_context.go ├── parser.go ├── parser_test.go ├── release-it.sh ├── resources ├── dep │ ├── dependency.proto │ ├── dependency2.proto │ ├── dependent.proto │ └── dependent2.proto ├── descriptor.proto ├── enum.proto ├── erroneous │ ├── dummy.proto │ ├── dup-enum-constant.proto │ ├── dup-enum.proto │ ├── dup-msg.proto │ ├── dup-nested-msg.proto │ ├── enum-constant-same-tag.proto │ ├── enum-in-wrong-context.proto │ ├── extend-in-wrong-context.proto │ ├── import-in-wrong-context.proto │ ├── missing-bracket-enum.proto │ ├── missing-bracket-msg.proto │ ├── missing-msg.proto │ ├── missing-package.proto │ ├── msg-in-wrong-context.proto │ ├── no-syntax.proto │ ├── oneof-in-wrong-context.proto │ ├── optional-in-proto3.proto │ ├── package-in-wrong-context.proto │ ├── required-in-proto3.proto │ ├── rpc-in-wrong-context.proto │ ├── syntax-in-wrong-context.proto │ ├── unused-import.proto │ ├── wrong-enum-constant-tag.proto │ ├── wrong-extend.proto │ ├── wrong-field.proto │ ├── wrong-import.proto │ ├── wrong-import2.proto │ ├── wrong-import3.proto │ ├── wrong-inline-option.proto │ ├── wrong-label-in-oneof-field.proto │ ├── wrong-map-declaration.proto │ ├── wrong-map-in-oneof.proto │ ├── wrong-map-key.proto │ ├── wrong-map-key2.proto │ ├── wrong-map-labels.proto │ ├── wrong-msg.proto │ ├── wrong-oneof.proto │ ├── wrong-option.proto │ ├── wrong-option2.proto │ ├── wrong-public-import.proto │ ├── wrong-rpc-datatype.proto │ ├── wrong-rpc.proto │ ├── wrong-rpc2.proto │ ├── wrong-service.proto │ ├── wrong-syntax.proto │ ├── wrong-syntax2.proto │ └── wrong-syntax3.proto ├── internal │ ├── README.txt │ ├── ext │ │ └── privatex.proto │ ├── proto2_test.proto │ ├── proto3_test.proto │ └── publicx.proto └── service.proto └── verifier.go /.gitignore: -------------------------------------------------------------------------------- 1 | pbparser 2 | pbparser.test 3 | debug* 4 | 5 | coverage.out 6 | cpuprof* 7 | memprof* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run pbparser", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "debug", 9 | "remotePath": "", 10 | "port": 2345, 11 | "host": "127.0.0.1", 12 | "program": "${workspaceRoot}", 13 | "env": {}, 14 | "args": [], 15 | "showLog": true 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shahid Khan 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tallstoat/pbparser.svg?branch=master)](https://travis-ci.org/tallstoat/pbparser) 2 | [![GoReportCard](https://goreportcard.com/badge/github.com/tallstoat/pbparser)](https://goreportcard.com/report/github.com/tallstoat/pbparser) 3 | [![GoDoc](https://godoc.org/github.com/tallstoat/pbparser?status.svg)](https://godoc.org/github.com/tallstoat/pbparser) 4 | 5 | # pbparser 6 | 7 | Pbparser is a library for parsing protocol buffer (".proto") files. 8 | 9 | ## Why? 10 | 11 | Protocol buffers are a flexible and efficient mechanism for serializing structured data. 12 | The Protbuf compiler (protoc) is *the source of truth* when it comes to parsing proto files. 13 | However protoc can be challenging to use in some scenarios :- 14 | 15 | * Protoc can be invoked by spawning a process from go code. If the caller now relies on the output of the compiler, they would have to parse the messages on stdout. This is fine for situations which need mere validations of proto files but does not work for usecases which require a standard defined parsed output structure to work with. 16 | * Protoc can also be invoked with *--descriptor_set_out* option to write out the proto file as a FileDescriptorSet (a protocol buffer defined in descriptor.proto). Ideally, this should have been sufficient. However, this again requires one to write a text parser to parse it. 17 | 18 | This parser library is meant to address the above mentioned challenges. 19 | 20 | ## Installing 21 | 22 | Using pbparser is easy. First, use `go get` to install the latest version of the library. 23 | 24 | ``` 25 | go get -u github.com/tallstoat/pbparser 26 | ``` 27 | 28 | Next, include pbparser in your application code. 29 | 30 | ```go 31 | import "github.com/tallstoat/pbparser" 32 | ``` 33 | 34 | ## APIs 35 | 36 | This library exposes two apis. Both the apis return a ProtoFile datastructure and a non-nil Error if there is an issue in the parse operation itself or the subsequent validations. 37 | 38 | ```go 39 | func Parse(r io.Reader, p ImportModuleProvider) (ProtoFile, error) 40 | ``` 41 | 42 | The Parse() function expects the client code to provide a reader for the protobuf content and also a ImportModuleProvider which can be used to callback the client code for any imports in the protobuf content. If there are no imports, the client can choose to pass this as nil. 43 | 44 | ```go 45 | func ParseFile(file string) (ProtoFile, error) 46 | ``` 47 | 48 | The ParseFile() function is a utility function which expects the client code to provide only the path of the protobuf file. If there are any imports in the protobuf file, the parser will look for them in the same directory where the protobuf file resides. 49 | 50 | ## Choosing an API 51 | 52 | Clients should use the Parse() function if they are not comfortable with letting the pbparser library access the disk directly. This function should also be preferred if the imports in the protobuf file are accessible to the client code but the client code does not want to give pbparser direct access to them. In such cases, the client code has to construct a ImportModuleProvider instance and pass it to the library. This instance must know how to resolve a given "import" and provide a reader for it. 53 | 54 | On the other hand, Clients should use the ParseFile() function if all the imported files as well as the protobuf file are on disk relative to the directory in which the protobuf file resides and they are comfortable with letting the pbparser library access the disk directly. 55 | 56 | ## Usage 57 | 58 | Please refer to the [examples](https://godoc.org/github.com/tallstoat/pbparser#pkg-examples) for API usage. 59 | 60 | ## Issues 61 | 62 | If you run into any issues or have enhancement suggestions, please create an issue [here](https://github.com/tallstoat/pbparser/issues). 63 | 64 | However I would much prefer PRs since at this time I'm unable to work on issues :-/ 65 | 66 | ## Contributing 67 | 68 | 1. Fork this repo. 69 | 2. Create your feature branch (`git checkout -b my-new-feature`). 70 | 3. Commit your changes (`git commit -am 'Add some feature'`). 71 | 4. Push to the branch (`git push origin my-new-feature`). 72 | 5. Create new Pull Request. 73 | 74 | ## License 75 | 76 | Pbparser is released under the MIT license. See [LICENSE](https://github.com/tallstoat/pbparser/blob/master/LICENSE) 77 | 78 | -------------------------------------------------------------------------------- /datatype.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // DataTypeCategory is an enumeration which represents the possible kinds 10 | // of field datatypes in message, oneof and extend declaration constructs. 11 | type DataTypeCategory int 12 | 13 | const ( 14 | ScalarDataTypeCategory DataTypeCategory = iota 15 | MapDataTypeCategory 16 | NamedDataTypeCategory 17 | ) 18 | 19 | // DataType is the interface which must be implemented by the field datatypes. 20 | // Name() returns the name of the datatype and Category() returns the category 21 | // of the datatype. 22 | type DataType interface { 23 | Name() string 24 | Category() DataTypeCategory 25 | } 26 | 27 | // ScalarType is an enumeration which represents all known supported scalar 28 | // field datatypes. 29 | type ScalarType int 30 | 31 | const ( 32 | AnyScalar ScalarType = iota + 1 33 | BoolScalar 34 | BytesScalar 35 | DoubleScalar 36 | FloatScalar 37 | Fixed32Scalar 38 | Fixed64Scalar 39 | Int32Scalar 40 | Int64Scalar 41 | Sfixed32Scalar 42 | Sfixed64Scalar 43 | Sint32Scalar 44 | Sint64Scalar 45 | StringScalar 46 | Uint32Scalar 47 | Uint64Scalar 48 | ) 49 | 50 | var scalarLookupMap = map[string]ScalarType{ 51 | "any": AnyScalar, 52 | "bool": BoolScalar, 53 | "bytes": BytesScalar, 54 | "double": DoubleScalar, 55 | "float": FloatScalar, 56 | "fixed32": Fixed32Scalar, 57 | "fixed64": Fixed64Scalar, 58 | "int32": Int32Scalar, 59 | "int64": Int64Scalar, 60 | "sfixed32": Sfixed32Scalar, 61 | "sfixed64": Sfixed64Scalar, 62 | "sint32": Sint32Scalar, 63 | "sint64": Sint64Scalar, 64 | "string": StringScalar, 65 | "uint32": Uint32Scalar, 66 | "uint64": Uint64Scalar, 67 | } 68 | 69 | // ScalarDataType is a construct which represents 70 | // all supported protobuf scalar datatypes. 71 | type ScalarDataType struct { 72 | scalarType ScalarType 73 | name string 74 | } 75 | 76 | // Name function implementation of interface DataType for ScalarDataType 77 | func (sdt ScalarDataType) Name() string { 78 | return sdt.name 79 | } 80 | 81 | // Category function implementation of interface DataType for ScalarDataType 82 | func (sdt ScalarDataType) Category() DataTypeCategory { 83 | return ScalarDataTypeCategory 84 | } 85 | 86 | // NewScalarDataType creates and returns a new ScalarDataType for the given string. 87 | // If a scalar data type mapping does not exist for the given string, an Error is returned. 88 | func NewScalarDataType(s string) (ScalarDataType, error) { 89 | key := strings.ToLower(s) 90 | st := scalarLookupMap[key] 91 | if st == 0 { 92 | msg := fmt.Sprintf("'%v' is not a valid ScalarDataType", s) 93 | return ScalarDataType{}, errors.New(msg) 94 | } 95 | return ScalarDataType{name: key, scalarType: st}, nil 96 | } 97 | 98 | // MapDataType is a construct which represents a protobuf map datatype. 99 | type MapDataType struct { 100 | keyType DataType 101 | valueType DataType 102 | } 103 | 104 | // Name function implementation of interface DataType for MapDataType 105 | func (mdt MapDataType) Name() string { 106 | return "map<" + mdt.keyType.Name() + ", " + mdt.valueType.Name() + ">" 107 | } 108 | 109 | // Category function implementation of interface DataType for MapDataType 110 | func (mdt MapDataType) Category() DataTypeCategory { 111 | return MapDataTypeCategory 112 | } 113 | 114 | // NamedDataType is a construct which represents a message datatype as 115 | // a RPC request or response and a message/enum datatype as a field in 116 | // message, oneof or extend declarations. 117 | type NamedDataType struct { 118 | supportsStreaming bool 119 | name string 120 | } 121 | 122 | // Name function implementation of interface DataType for NamedDataType 123 | func (ndt NamedDataType) Name() string { 124 | return ndt.name 125 | } 126 | 127 | // Category function implementation of interface DataType for NamedDataType 128 | func (ndt NamedDataType) Category() DataTypeCategory { 129 | return NamedDataTypeCategory 130 | } 131 | 132 | // IsStream returns true if the NamedDataType is being used in a rpc 133 | // as a request or response and is preceded by a Stream keyword. 134 | func (ndt NamedDataType) IsStream() bool { 135 | return ndt.supportsStreaming 136 | } 137 | 138 | // stream marks a NamedDataType as being preceded by a Stream keyword. 139 | func (ndt *NamedDataType) stream(flag bool) { 140 | ndt.supportsStreaming = flag 141 | } 142 | -------------------------------------------------------------------------------- /datatype_test.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestScalarDataTypeCreationViaName(t *testing.T) { 9 | var tests = []struct { 10 | s string 11 | }{ 12 | {s: "any"}, 13 | {s: "int32"}, 14 | {s: "duh"}, 15 | } 16 | 17 | for _, tt := range tests { 18 | x, err := NewScalarDataType(tt.s) 19 | if err != nil { 20 | fmt.Println(err.Error()) 21 | } else { 22 | fmt.Printf("Scalar Data Type: %v created for input string: %v \n", x, tt.s) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package pbparser is a library for parsing protocol buffer (".proto") files. 3 | 4 | It exposes two apis for parsing protocol buffer files. Both the apis return a 5 | ProtoFile datastructure and a non-nil Error if there is an issue. 6 | 7 | After the parsing operation, this library also validates any references to 8 | imported constructs i.e. any references to imported enums, messages etc in the 9 | file match the definitions in the imported modules. 10 | 11 | API 12 | 13 | Clients should invoke the following apis :- 14 | 15 | func Parse(r io.Reader, p ImportModuleProvider) (ProtoFile, error) 16 | 17 | The Parse() function expects the client code to provide a reader for the protobuf content 18 | and also a ImportModuleProvider which can be used to callback the client code for any 19 | imports in the protobuf content. If there are no imports, the client can choose to pass 20 | this as nil. 21 | 22 | func ParseFile(file string) (ProtoFile, error) 23 | 24 | The ParseFile() function is a utility function which expects the client code to provide only the path 25 | of the protobuf file. If there are any imports in the protobuf file, the parser will look for them 26 | in the same directory where the protobuf file resides. 27 | 28 | Choosing an API 29 | 30 | Clients should use the Parse() function if they are not comfortable with letting the pbparser library 31 | access the disk directly. This function should also be preferred if the imports in the protobuf file 32 | are accessible to the client code but the client code does not want to give pbparser direct access to 33 | them. In such cases, the client code has to construct a ImportModuleProvider instance and pass it to 34 | the library. This instance must know how to resolve a given "import" and provide a reader for it. 35 | 36 | On the other hand, Clients should use the ParseFile() function if all the imported files as well as the 37 | protobuf file are on disk relative to the directory in which the protobuf file resides and they are 38 | comfortable with letting the pbparser library access the disk directly. 39 | 40 | ProtoFile datastructure 41 | 42 | This datastructure represents parsed model of the given protobuf file. It includes the following information :- 43 | 44 | type ProtoFile struct { 45 | PackageName string // name of the package 46 | Syntax string // the protocol buffer syntax 47 | Dependencies []string // names of any imports 48 | PublicDependencies []string // names of any public imports 49 | Options []OptionElement // any package level options 50 | Enums []EnumElement // any defined enums 51 | Messages []MessageElement // any defined messages 52 | Services []ServiceElement // any defined services 53 | ExtendDeclarations []ExtendElement // any extends directives 54 | } 55 | 56 | Each attribute in turn has a defined structure, which is explained in the godoc of the corresponding elements. 57 | 58 | Design Considerations 59 | 60 | This library consciously chooses to log no information on it's own. Any failures are communicated 61 | back to client code via the returned Error. 62 | 63 | In case of a parsing error, it returns an Error back to the client with a line and column number in the file 64 | on which the parsing error was encountered. 65 | 66 | In case of a post-parsing validation error, it returns an Error with enough information to 67 | identify the erroneous protobuf construct. 68 | 69 | */ 70 | package pbparser 71 | -------------------------------------------------------------------------------- /elements.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | // OptionElement is a datastructure which models 4 | // the option construct in a protobuf file. Option constructs 5 | // exist at various levels/contexts like file, message etc. 6 | type OptionElement struct { 7 | Name string 8 | Value string 9 | IsParenthesized bool 10 | } 11 | 12 | // EnumConstantElement is a datastructure which models 13 | // the fields within an enum construct. Enum constants can 14 | // also have inline options specified. 15 | type EnumConstantElement struct { 16 | Name string 17 | Documentation string 18 | Options []OptionElement 19 | Tag int 20 | } 21 | 22 | // EnumElement is a datastructure which models 23 | // the enum construct in a protobuf file. Enums are 24 | // defined standalone or as nested entities within messages. 25 | type EnumElement struct { 26 | Name string 27 | QualifiedName string 28 | Documentation string 29 | Options []OptionElement 30 | EnumConstants []EnumConstantElement 31 | } 32 | 33 | // RPCElement is a datastructure which models 34 | // the rpc construct in a protobuf file. RPCs are defined 35 | // nested within ServiceElements. 36 | type RPCElement struct { 37 | Name string 38 | Documentation string 39 | Options []OptionElement 40 | RequestType NamedDataType 41 | ResponseType NamedDataType 42 | } 43 | 44 | // ServiceElement is a datastructure which models 45 | // the service construct in a protobuf file. Service 46 | // construct defines the rpcs (apis) for the service. 47 | type ServiceElement struct { 48 | Name string 49 | QualifiedName string 50 | Documentation string 51 | Options []OptionElement 52 | RPCs []RPCElement 53 | } 54 | 55 | // FieldElement is a datastructure which models 56 | // a field of a message, a field of a oneof element 57 | // or an entry in the extend declaration in a protobuf file. 58 | type FieldElement struct { 59 | Name string 60 | Documentation string 61 | Options []OptionElement 62 | Label string /* optional, required, repeated, oneof */ 63 | Type DataType 64 | Tag int 65 | } 66 | 67 | // OneOfElement is a datastructure which models 68 | // a oneoff construct in a protobuf file. All the fields in a 69 | // oneof construct share memory, and at most one field can be 70 | // set at any time. 71 | type OneOfElement struct { 72 | Name string 73 | Documentation string 74 | Options []OptionElement 75 | Fields []FieldElement 76 | } 77 | 78 | // ExtensionsElement is a datastructure which models 79 | // an extensions construct in a protobuf file. An extension 80 | // is a placeholder for a field whose type is not defined by the 81 | // original .proto file. This allows other .proto files to add 82 | // to the original message definition by defining field ranges which 83 | // can be used for extensions. 84 | type ExtensionsElement struct { 85 | Documentation string 86 | Start int 87 | End int 88 | } 89 | 90 | // ReservedRangeElement is a datastructure which models 91 | // a reserved construct in a protobuf message. 92 | type ReservedRangeElement struct { 93 | Documentation string 94 | Start int 95 | End int 96 | } 97 | 98 | // MessageElement is a datastructure which models 99 | // the message construct in a protobuf file. 100 | type MessageElement struct { 101 | Name string 102 | QualifiedName string 103 | Documentation string 104 | Options []OptionElement 105 | Fields []FieldElement 106 | Enums []EnumElement 107 | Messages []MessageElement 108 | OneOfs []OneOfElement 109 | ExtendDeclarations []ExtendElement 110 | Extensions []ExtensionsElement 111 | ReservedRanges []ReservedRangeElement 112 | ReservedNames []string 113 | } 114 | 115 | // ExtendElement is a datastructure which models 116 | // the extend construct in a protobuf file which is used 117 | // to add new fields to a previously declared message type. 118 | type ExtendElement struct { 119 | Name string 120 | QualifiedName string 121 | Documentation string 122 | Fields []FieldElement 123 | } 124 | 125 | // ProtoFile is a datastructure which represents the parsed model 126 | // of the given protobuf file. 127 | // 128 | // It includes the package name, the syntax, the import dependencies, 129 | // any public import dependencies, any options, enums, messages, services, 130 | // extension declarations etc. 131 | // 132 | // This is populated by the parser & post-validation returned to the 133 | // client code. 134 | type ProtoFile struct { 135 | PackageName string 136 | Syntax string 137 | Dependencies []string 138 | PublicDependencies []string 139 | Options []OptionElement 140 | Enums []EnumElement 141 | Messages []MessageElement 142 | Services []ServiceElement 143 | ExtendDeclarations []ExtendElement 144 | } 145 | -------------------------------------------------------------------------------- /example_parse_test.go: -------------------------------------------------------------------------------- 1 | package pbparser_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/tallstoat/pbparser" 12 | ) 13 | 14 | // Example code for the Parse() API 15 | func Example_parse() { 16 | // read the proto file contents from disk & create a reader 17 | file := "./examples/mathservice.proto" 18 | raw, err := ioutil.ReadFile(file) 19 | if err != nil { 20 | fmt.Printf("Unable to read proto file: %v \n", err) 21 | os.Exit(-1) 22 | } 23 | r := strings.NewReader(string(raw[:])) 24 | 25 | // implement a dir based import module provider which reads 26 | // import modules from the same dir as the original proto file 27 | dir := filepath.Dir(file) 28 | pr := DirBasedImportModuleProvider{dir: dir} 29 | 30 | // invoke Parse() API to parse the file 31 | pf, err := pbparser.Parse(r, &pr) 32 | if err != nil { 33 | fmt.Printf("Unable to parse proto file: %v \n", err) 34 | os.Exit(-1) 35 | } 36 | 37 | // print attributes of the returned datastructure 38 | fmt.Printf("PackageName: %v, Syntax: %v\n", pf.PackageName, pf.Syntax) 39 | } 40 | 41 | // DirBasedImportModuleProvider is a import module provider which looks for import 42 | // modules in the dir that it was initialized with. 43 | type DirBasedImportModuleProvider struct { 44 | dir string 45 | } 46 | 47 | func (pi *DirBasedImportModuleProvider) Provide(module string) (io.Reader, error) { 48 | modulePath := pi.dir + string(filepath.Separator) + module 49 | 50 | // read the module file contents from dir & create a reader... 51 | raw, err := ioutil.ReadFile(modulePath) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return strings.NewReader(string(raw[:])), nil 57 | } 58 | -------------------------------------------------------------------------------- /example_parsefile_test.go: -------------------------------------------------------------------------------- 1 | package pbparser_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/tallstoat/pbparser" 8 | ) 9 | 10 | // Example code for the ParseFile() API 11 | func Example_parseFile() { 12 | file := "./examples/mathservice.proto" 13 | 14 | // invoke ParseFile() API to parse the file 15 | pf, err := pbparser.ParseFile(file) 16 | if err != nil { 17 | fmt.Printf("Unable to parse proto file: %v \n", err) 18 | os.Exit(-1) 19 | } 20 | 21 | // print attributes of the returned datastructure 22 | fmt.Printf("PackageName: %v, Syntax: %v\n", pf.PackageName, pf.Syntax) 23 | } 24 | -------------------------------------------------------------------------------- /examples/internal/numbers.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package numbers; 3 | 4 | // Numbers is what you provide as input to a mathematical operation 5 | message Numbers { 6 | int32 first = 1; 7 | int32 second = 2; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /examples/mathservice.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package math; 3 | 4 | option java_package = "com.google.protobuf"; 5 | 6 | import "internal/numbers.proto"; 7 | 8 | // MathService is a service which performs simple mathematical operations 9 | service MathService { 10 | rpc Add(numbers.Numbers) returns (Result) {} 11 | rpc Multiply(numbers.Numbers) returns (Result) {} 12 | } 13 | 14 | // Result is what you get post a mathematical operation 15 | message Result { 16 | int32 output = 1; 17 | string message = 2; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /examples_validator_test.go: -------------------------------------------------------------------------------- 1 | package pbparser_test 2 | 3 | import "testing" 4 | 5 | // NOTE: Validates the examples which are a part of godoc 6 | // to ensure that they are working as exected and are not 7 | // broken! 8 | func TestParse(t *testing.T) { 9 | Example_parse() 10 | Example_parseFile() 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tallstoat/pbparser 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /import_module_provider.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // ImportModuleProvider is the interface which given a protobuf import module returns a reader for it. 11 | // 12 | // The import module could be on disk or elsewhere. In order for the pbparser library to not be tied in 13 | // to a specific method of reading the import modules, it exposes this interface to the clients. The clients 14 | // must provide a implementation of this interface which knows how to interpret the module string & returns a 15 | // reader for the module. This is needed if the client is calling the Parse() function of the pbparser library. 16 | // 17 | // If the client knows the import modules are on disk, they can instead call the ParseFile() function which 18 | // internally creates a default import module reader which performs disk access to load the contents of the 19 | // dependency modules. 20 | type ImportModuleProvider interface { 21 | Provide(module string) (io.Reader, error) 22 | } 23 | 24 | // defaultImportModuleProviderImpl is default implementation of the ImportModuleProvider interface. 25 | // 26 | // This is used internally by the pbparser library to load import modules from disk. 27 | type defaultImportModuleProviderImpl struct { 28 | dir string 29 | } 30 | 31 | func (pi *defaultImportModuleProviderImpl) Provide(module string) (io.Reader, error) { 32 | modulePath := pi.dir + string(filepath.Separator) + module 33 | 34 | // read the module file contents & create a reader... 35 | raw, err := ioutil.ReadFile(modulePath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | r := strings.NewReader(string(raw[:])) 41 | return r, nil 42 | } 43 | -------------------------------------------------------------------------------- /parse_context.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | // The parsing context. We need to pass this around during parsing 4 | // to distinguish between various cases when the same code gets executed 5 | // recursively as well as to pass in objects to hang nested objects to. 6 | type parseCtx struct { 7 | obj interface{} 8 | ctxType ctxType 9 | } 10 | 11 | // Type of context 12 | type ctxType int 13 | 14 | // The various context types 15 | const ( 16 | fileCtx ctxType = iota 17 | msgCtx 18 | oneOfCtx 19 | enumCtx 20 | rpcCtx 21 | extendCtx 22 | serviceCtx 23 | ) 24 | 25 | var ctxTypeToStringMap = [...]string{ 26 | fileCtx: "file", 27 | msgCtx: "message", 28 | oneOfCtx: "oneof", 29 | enumCtx: "enum", 30 | rpcCtx: "rpc", 31 | extendCtx: "extend", 32 | serviceCtx: "service", 33 | } 34 | 35 | // the veritable tostring() for java lovers... 36 | func (pc parseCtx) String() string { 37 | return ctxTypeToStringMap[pc.ctxType] 38 | } 39 | 40 | // does this ctx permit package support? 41 | func (pc parseCtx) permitsPackage() bool { 42 | return pc.ctxType == fileCtx 43 | } 44 | 45 | // does this ctx permit syntax support? 46 | func (pc parseCtx) permitsSyntax() bool { 47 | return pc.ctxType == fileCtx 48 | } 49 | 50 | // does this ctx permit import support? 51 | func (pc parseCtx) permitsImport() bool { 52 | return pc.ctxType == fileCtx 53 | } 54 | 55 | // does this ctx permit field support? 56 | func (pc parseCtx) permitsField() bool { 57 | return pc.ctxType == msgCtx || pc.ctxType == oneOfCtx || pc.ctxType == extendCtx 58 | } 59 | 60 | // does this ctx permit option? 61 | func (pc parseCtx) permitsOption() bool { 62 | return pc.ctxType == fileCtx || 63 | pc.ctxType == msgCtx || 64 | pc.ctxType == oneOfCtx || 65 | pc.ctxType == enumCtx || 66 | pc.ctxType == serviceCtx || 67 | pc.ctxType == rpcCtx 68 | } 69 | 70 | // does this ctx permit extensions support? 71 | func (pc parseCtx) permitsExtensions() bool { 72 | return pc.ctxType == msgCtx 73 | } 74 | 75 | // does this ctx permit extend declarations? 76 | func (pc parseCtx) permitsExtend() bool { 77 | return pc.ctxType == fileCtx || pc.ctxType == msgCtx 78 | } 79 | 80 | // does this ctx permit reserved keyword support? 81 | func (pc parseCtx) permitsReserved() bool { 82 | return pc.ctxType == msgCtx 83 | } 84 | 85 | // does this ctx permit rpc support? 86 | func (pc parseCtx) permitsRPC() bool { 87 | return pc.ctxType == serviceCtx 88 | } 89 | 90 | // does this ctx permit OneOf support? 91 | func (pc parseCtx) permitsOneOf() bool { 92 | return pc.ctxType == msgCtx 93 | } 94 | 95 | // does this ctx permit enum support? 96 | func (pc parseCtx) permitsEnum() bool { 97 | return pc.ctxType == fileCtx || pc.ctxType == msgCtx 98 | } 99 | 100 | // does this ctx permit msg support? 101 | func (pc parseCtx) permitsMsg() bool { 102 | return pc.ctxType == fileCtx || pc.ctxType == msgCtx 103 | } 104 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // Parse function parses the protobuf content passed to it by the the client code via 17 | // the reader. It also uses the passed-in ImportModuleProvider to callback the client 18 | // code for any imports in the protobuf content. If there are no imports, the client 19 | // can choose to pass this as nil. 20 | // 21 | // This function returns populated ProtoFile struct if parsing is successful. 22 | // If the parsing or validation fails, it returns an Error. 23 | func Parse(r io.Reader, p ImportModuleProvider) (ProtoFile, error) { 24 | if r == nil { 25 | return ProtoFile{}, errors.New("Reader for protobuf content is mandatory") 26 | } 27 | 28 | pf := ProtoFile{} 29 | 30 | // parse the main proto file... 31 | if err := parse(r, &pf); err != nil { 32 | return pf, err 33 | } 34 | 35 | // verify via extra checks... 36 | if err := verify(&pf, p); err != nil { 37 | return pf, err 38 | } 39 | 40 | return pf, nil 41 | } 42 | 43 | // ParseFile function reads and parses the content of the protobuf file whose 44 | // path is provided as sole argument to the function. If there are any imports 45 | // in the protobuf file, the parser will look for them in the same directory 46 | // where the protobuf file resides. 47 | // 48 | // This function returns populated ProtoFile struct if parsing is successful. 49 | // If the parsing or validation fails, it returns an Error. 50 | func ParseFile(file string) (ProtoFile, error) { 51 | if file == "" { 52 | return ProtoFile{}, errors.New("File is mandatory") 53 | } 54 | 55 | // read the proto file contents & create a reader... 56 | raw, err := ioutil.ReadFile(file) 57 | if err != nil { 58 | return ProtoFile{}, err 59 | } 60 | r := strings.NewReader(string(raw[:])) 61 | 62 | // create default import module provider... 63 | dir := filepath.Dir(file) 64 | impr := defaultImportModuleProviderImpl{dir: dir} 65 | 66 | return Parse(r, &impr) 67 | } 68 | 69 | // parse is an internal function which is invoked with the reader for the main proto file 70 | // & a pointer to the ProtoFile struct to be populated post parsing & verification. 71 | func parse(r io.Reader, pf *ProtoFile) error { 72 | br := bufio.NewReader(r) 73 | 74 | // initialize parser... 75 | loc := location{line: 1, column: 0} 76 | parser := parser{br: br, loc: &loc} 77 | 78 | // parse the file contents... 79 | return parser.parse(pf) 80 | } 81 | 82 | // This struct tracks current location of the parse process. 83 | type location struct { 84 | column int 85 | line int 86 | } 87 | 88 | // The parser. This struct has all the functions which actually perform the 89 | // job of parsing inputs from a specified reader. 90 | type parser struct { 91 | br *bufio.Reader 92 | loc *location 93 | eofReached bool // We set this flag, when eof is encountered 94 | prefix string // The current package name + nested type names, separated by dots 95 | lastColumnRead int 96 | } 97 | 98 | // This function just looks for documentation and 99 | // then declaration in a loop till EOF is reached 100 | func (p *parser) parse(pf *ProtoFile) error { 101 | for { 102 | // read any documentation if found... 103 | documentation, err := p.readDocumentationIfFound() 104 | if err != nil { 105 | return err 106 | } 107 | if p.eofReached { 108 | break 109 | } 110 | 111 | // skip any intervening whitespace if present... 112 | p.skipWhitespace() 113 | if p.eofReached { 114 | break 115 | } 116 | 117 | // read any declaration... 118 | err = p.readDeclaration(pf, documentation, parseCtx{ctxType: fileCtx}) 119 | if err != nil { 120 | return err 121 | } 122 | if p.eofReached { 123 | break 124 | } 125 | } 126 | return nil 127 | } 128 | 129 | func (p *parser) readDocumentationIfFound() (string, error) { 130 | for { 131 | c := p.read() 132 | if c == eof { 133 | p.eofReached = true 134 | return "", nil 135 | } else if isWhitespace(c) { 136 | p.skipWhitespace() 137 | continue 138 | } else if isStartOfComment(c) { 139 | documentation, err := p.readDocumentation() 140 | if err != nil { 141 | return "", err 142 | } 143 | return documentation, nil 144 | } 145 | // this is not documentation, break out of the loop... 146 | p.unread() 147 | break 148 | } 149 | return "", nil 150 | } 151 | 152 | func (p *parser) readDeclaration(pf *ProtoFile, documentation string, ctx parseCtx) error { 153 | // Skip unnecessary semicolons... 154 | c := p.read() 155 | if c == ';' { 156 | return nil 157 | } 158 | p.unread() 159 | 160 | // Read next label... 161 | label := p.readWord() 162 | if label == "package" { 163 | if !ctx.permitsPackage() { 164 | return p.unexpected(label, ctx) 165 | } 166 | p.skipWhitespace() 167 | pf.PackageName = p.readWord() 168 | p.prefix = pf.PackageName + "." 169 | } else if label == "syntax" { 170 | if !ctx.permitsSyntax() { 171 | return p.unexpected(label, ctx) 172 | } 173 | return p.readSyntax(pf) 174 | } else if label == "import" { 175 | if !ctx.permitsImport() { 176 | return p.unexpected(label, ctx) 177 | } 178 | return p.readImport(pf) 179 | } else if label == "option" { 180 | if !ctx.permitsOption() { 181 | return p.unexpected(label, ctx) 182 | } 183 | return p.readOption(pf, documentation, ctx) 184 | } else if label == "message" { 185 | if !ctx.permitsMsg() { 186 | return p.unexpected(label, ctx) 187 | } 188 | return p.readMessage(pf, documentation, ctx) 189 | } else if label == "enum" { 190 | if !ctx.permitsEnum() { 191 | return p.unexpected(label, ctx) 192 | } 193 | return p.readEnum(pf, documentation, ctx) 194 | } else if label == "extend" { 195 | if !ctx.permitsExtend() { 196 | return p.unexpected(label, ctx) 197 | } 198 | return p.readExtend(pf, documentation, ctx) 199 | } else if label == "service" { 200 | return p.readService(pf, documentation) 201 | } else if label == "rpc" { 202 | if !ctx.permitsRPC() { 203 | return p.unexpected(label, ctx) 204 | } 205 | se := ctx.obj.(*ServiceElement) 206 | return p.readRPC(pf, se, documentation) 207 | } else if label == "oneof" { 208 | if !ctx.permitsOneOf() { 209 | return p.unexpected(label, ctx) 210 | } 211 | return p.readOneOf(pf, documentation, ctx) 212 | } else if label == "extensions" { 213 | if !ctx.permitsExtensions() { 214 | return p.unexpected(label, ctx) 215 | } 216 | return p.readExtensions(pf, documentation, ctx) 217 | } else if label == "reserved" { 218 | if !ctx.permitsReserved() { 219 | return p.unexpected(label, ctx) 220 | } 221 | return p.readReserved(pf, documentation, ctx) 222 | } else if ctx.ctxType == msgCtx || ctx.ctxType == extendCtx || ctx.ctxType == oneOfCtx { 223 | if !ctx.permitsField() { 224 | return p.errline("fields must be nested") 225 | } 226 | return p.readField(pf, label, documentation, ctx) 227 | } else if ctx.ctxType == enumCtx { 228 | return p.readEnumConstant(pf, label, documentation, ctx) 229 | } else if label != "" { 230 | return p.unexpected(label, ctx) 231 | } 232 | return nil 233 | } 234 | 235 | func (p *parser) readDeclarationsInLoop(pf *ProtoFile, ctx parseCtx) error { 236 | for { 237 | doc, err := p.readDocumentationIfFound() 238 | if err != nil { 239 | return err 240 | } 241 | p.skipWhitespace() 242 | if p.eofReached { 243 | return fmt.Errorf("Reached end of input in %v definition (missing '}')", ctx) 244 | } 245 | if c := p.read(); c == '}' { 246 | break 247 | } 248 | p.unread() 249 | 250 | if err = p.readDeclaration(pf, doc, ctx); err != nil { 251 | return err 252 | } 253 | } 254 | return nil 255 | } 256 | 257 | func (p *parser) readReserved(pf *ProtoFile, documentation string, ctx parseCtx) error { 258 | me := ctx.obj.(*MessageElement) 259 | 260 | p.skipWhitespace() 261 | c := p.read() 262 | p.unread() 263 | 264 | if isDigit(c) { 265 | if err := p.readReservedRanges(documentation, me); err != nil { 266 | return err 267 | } 268 | } else { 269 | if err := p.readReservedNames(documentation, me); err != nil { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | 276 | func (p *parser) readReservedRanges(documentation string, me *MessageElement) error { 277 | for { 278 | start, err := p.readInt() 279 | if err != nil { 280 | return err 281 | } 282 | 283 | rr := ReservedRangeElement{Start: start, End: start, Documentation: documentation} 284 | 285 | // check if we are done providing the reserved names 286 | c := p.read() 287 | if c == ';' { 288 | me.ReservedRanges = append(me.ReservedRanges, rr) 289 | break 290 | } else if c == ',' { 291 | me.ReservedRanges = append(me.ReservedRanges, rr) 292 | p.skipWhitespace() 293 | } else { 294 | p.unread() 295 | p.skipWhitespace() 296 | if w := p.readWord(); w != "to" { 297 | return p.errline("Expected 'to', but found: %v", w) 298 | } 299 | p.skipWhitespace() 300 | end, err := p.readInt() 301 | if err != nil { 302 | return err 303 | } 304 | rr.End = end 305 | c2 := p.read() 306 | if c2 == ';' { 307 | me.ReservedRanges = append(me.ReservedRanges, rr) 308 | break 309 | } else if c2 == ',' { 310 | me.ReservedRanges = append(me.ReservedRanges, rr) 311 | p.skipWhitespace() 312 | } else { 313 | return p.errline("Expected ',' or ';', but found: %v", strconv.QuoteRune(c2)) 314 | } 315 | } 316 | } 317 | return nil 318 | } 319 | 320 | func (p *parser) readReservedNames(documentation string, me *MessageElement) error { 321 | for { 322 | name, err := p.readQuotedString(nil) 323 | if err != nil { 324 | return err 325 | } 326 | me.ReservedNames = append(me.ReservedNames, name) 327 | 328 | // check if we are done providing the reserved names 329 | c := p.read() 330 | if c == ';' { 331 | break 332 | } 333 | 334 | // if not, there should be more names provided after a comma... 335 | if c != ',' { 336 | return p.throw(',', c) 337 | } 338 | p.skipWhitespace() 339 | } 340 | return nil 341 | } 342 | 343 | func (p *parser) readField(pf *ProtoFile, label string, documentation string, ctx parseCtx) error { 344 | if label == optional && pf.Syntax == proto3 { 345 | return p.errline("Explicit 'optional' labels are disallowed in the proto3 syntax. " + 346 | "To define 'optional' fields in proto3, simply remove the 'optional' label, as fields " + 347 | "are 'optional' by default.") 348 | } else if label == required && pf.Syntax == proto3 { 349 | return p.errline("Required fields are not allowed in proto3") 350 | } else if label == required && ctx.ctxType == extendCtx { 351 | return p.errline("Message extensions cannot have required fields") 352 | } 353 | 354 | // the field struct... 355 | fe := FieldElement{Documentation: documentation} 356 | 357 | // figure out dataTypeStr based on the label... 358 | var err error 359 | dataTypeStr := label 360 | if label == required || label == optional || label == repeated { 361 | if ctx.ctxType == oneOfCtx { 362 | return p.errline("Label '%v' is disallowed in oneoff field", label) 363 | } 364 | fe.Label = label 365 | p.skipWhitespace() 366 | dataTypeStr = p.readWord() 367 | } 368 | 369 | // figure out the dataType 370 | if fe.Type, err = p.readDataTypeInternal(dataTypeStr); err != nil { 371 | return err 372 | } 373 | 374 | // perform checks for map data type... 375 | if fe.Type.Category() == MapDataTypeCategory { 376 | if fe.Label == repeated || fe.Label == required || fe.Label == optional { 377 | return p.errline("Label %v is not allowed on map fields", fe.Label) 378 | } 379 | if ctx.ctxType == oneOfCtx { 380 | return p.errline("Map fields are not allowed in oneofs") 381 | } 382 | if ctx.ctxType == extendCtx { 383 | return p.errline("Map fields are not allowed to be extensions") 384 | } 385 | mdt := fe.Type.(MapDataType) 386 | if mdt.keyType.Name() == "float" || mdt.keyType.Name() == "double" || mdt.keyType.Name() == "bytes" { 387 | return p.errline("Key in map fields cannot be float, double or bytes") 388 | } 389 | if mdt.keyType.Category() == NamedDataTypeCategory { 390 | return p.errline("Key in map fields cannot be a named type") 391 | } 392 | } 393 | 394 | // figure out the name 395 | p.skipWhitespace() 396 | if fe.Name, _, err = p.readName(); err != nil { 397 | return err 398 | } 399 | 400 | // check for equals sign... 401 | p.skipWhitespace() 402 | if c := p.read(); c != '=' { 403 | return p.throw('=', c) 404 | } 405 | 406 | // extract the field tag... 407 | p.skipWhitespace() 408 | if fe.Tag, err = p.readInt(); err != nil { 409 | return err 410 | } 411 | 412 | // If semicolon is next; we are done. If '[' is next, we must parse options for the field 413 | if fe.Options, err = p.readListOptionsOnALine(); err != nil { 414 | return err 415 | } 416 | 417 | // add field to the proper parent ... 418 | if ctx.ctxType == msgCtx { 419 | me := ctx.obj.(*MessageElement) 420 | me.Fields = append(me.Fields, fe) 421 | } else if ctx.ctxType == extendCtx { 422 | ee := ctx.obj.(*ExtendElement) 423 | ee.Fields = append(ee.Fields, fe) 424 | } else if ctx.ctxType == oneOfCtx { 425 | oe := ctx.obj.(*OneOfElement) 426 | oe.Fields = append(oe.Fields, fe) 427 | } 428 | return nil 429 | } 430 | 431 | // readListOptionsOnALine reads list options provided on a line. 432 | // generally relevant for fields and enum constant declarations. 433 | func (p *parser) readListOptionsOnALine() ([]OptionElement, error) { 434 | var err error 435 | var options []OptionElement 436 | p.skipWhitespace() 437 | c := p.read() 438 | if c == '[' { 439 | if options, err = p.readListOptions(); err != nil { 440 | return nil, err 441 | } 442 | c2 := p.read() 443 | if c2 != ';' { 444 | return nil, p.throw(';', c2) 445 | } 446 | } else if c != ';' { 447 | return nil, p.throw(';', c) 448 | } 449 | // Gobble up any inline documentation for a field 450 | p.skipUntilNewline() 451 | return options, nil 452 | } 453 | 454 | func (p *parser) readListOptions() ([]OptionElement, error) { 455 | var options []OptionElement 456 | optionsStr := p.readUntil(']') 457 | pairs := strings.Split(optionsStr, ",") 458 | for _, pair := range pairs { 459 | arr := strings.Split(pair, "=") 460 | if len(arr) != 2 { 461 | return nil, p.errline("Option '%v' is not specified as expected", arr) 462 | } 463 | oname, hasParenthesis := stripParenthesis(strings.TrimSpace(arr[0])) 464 | oval := stripQuotes(strings.TrimSpace(arr[1])) 465 | oe := OptionElement{Name: oname, Value: oval, IsParenthesized: hasParenthesis} 466 | options = append(options, oe) 467 | } 468 | return options, nil 469 | } 470 | 471 | func (p *parser) readOption(pf *ProtoFile, documentation string, ctx parseCtx) error { 472 | var err error 473 | var enc enclosure 474 | oe := OptionElement{} 475 | 476 | p.skipWhitespace() 477 | if oe.Name, enc, err = p.readName(); err != nil { 478 | return err 479 | } 480 | oe.IsParenthesized = (enc == parenthesis) 481 | 482 | p.skipWhitespace() 483 | if c := p.read(); c != '=' { 484 | return p.throw('=', c) 485 | } 486 | p.skipWhitespace() 487 | 488 | if p.read() == '"' { 489 | oe.Value = p.readUntil('"') 490 | } else { 491 | p.unread() 492 | oe.Value = p.readWord() 493 | } 494 | 495 | p.skipWhitespace() 496 | if c := p.read(); c != ';' { 497 | return p.throw(';', c) 498 | } 499 | 500 | // add option to the proper parent... 501 | if ctx.ctxType == msgCtx { 502 | me := ctx.obj.(*MessageElement) 503 | me.Options = append(me.Options, oe) 504 | } else if ctx.ctxType == oneOfCtx { 505 | ooe := ctx.obj.(*OneOfElement) 506 | ooe.Options = append(ooe.Options, oe) 507 | } else if ctx.ctxType == enumCtx { 508 | ee := ctx.obj.(*EnumElement) 509 | ee.Options = append(ee.Options, oe) 510 | } else if ctx.ctxType == serviceCtx { 511 | se := ctx.obj.(*ServiceElement) 512 | se.Options = append(se.Options, oe) 513 | } else if ctx.ctxType == rpcCtx { 514 | re := ctx.obj.(*RPCElement) 515 | re.Options = append(re.Options, oe) 516 | } else if ctx.ctxType == fileCtx { 517 | pf.Options = append(pf.Options, oe) 518 | } 519 | return nil 520 | } 521 | 522 | func (p *parser) readMessage(pf *ProtoFile, documentation string, ctx parseCtx) error { 523 | p.skipWhitespace() 524 | name, _, err := p.readName() 525 | if err != nil { 526 | return err 527 | } 528 | 529 | me := MessageElement{Name: name, QualifiedName: p.prefix + name, Documentation: documentation} 530 | 531 | // store previous prefix... 532 | var previousPrefix = p.prefix 533 | 534 | // update prefix... 535 | p.prefix = p.prefix + name + "." 536 | 537 | // reset prefix when we are done processing all fields in the message... 538 | defer func() { 539 | p.prefix = previousPrefix 540 | }() 541 | 542 | p.skipWhitespace() 543 | if c := p.read(); c != '{' { 544 | return p.throw('{', c) 545 | } 546 | 547 | innerCtx := parseCtx{ctxType: msgCtx, obj: &me} 548 | if err = p.readDeclarationsInLoop(pf, innerCtx); err != nil { 549 | return err 550 | } 551 | 552 | // add msg to the proper parent... 553 | if ctx.ctxType == msgCtx { 554 | parent := ctx.obj.(*MessageElement) 555 | parent.Messages = append(parent.Messages, me) 556 | } else { 557 | pf.Messages = append(pf.Messages, me) 558 | } 559 | return nil 560 | } 561 | 562 | func (p *parser) readExtensions(pf *ProtoFile, documentation string, ctx parseCtx) error { 563 | if pf.Syntax == proto3 { 564 | return p.errline("Extension ranges are not allowed in proto3") 565 | } 566 | 567 | p.skipWhitespace() 568 | start, err := p.readInt() 569 | if err != nil { 570 | return err 571 | } 572 | 573 | // At this point, make End be same as Start... 574 | xe := ExtensionsElement{Documentation: documentation, Start: start, End: start} 575 | 576 | c := p.read() 577 | if c != ';' { 578 | p.unread() 579 | p.skipWhitespace() 580 | if w := p.readWord(); w != "to" { 581 | return p.errline("Expected 'to', but found: %v", w) 582 | } 583 | p.skipWhitespace() 584 | var end int 585 | endStr := p.readWord() 586 | if endStr == "max" { 587 | end = 536870911 588 | } else { 589 | end, err = strconv.Atoi(endStr) 590 | if err != nil { 591 | return err 592 | } 593 | } 594 | xe.End = end 595 | } 596 | 597 | me := ctx.obj.(*MessageElement) 598 | me.Extensions = append(me.Extensions, xe) 599 | return nil 600 | } 601 | 602 | func (p *parser) readEnumConstant(pf *ProtoFile, label string, documentation string, ctx parseCtx) error { 603 | p.skipWhitespace() 604 | if c := p.read(); c != '=' { 605 | return p.throw('=', c) 606 | } 607 | p.skipWhitespace() 608 | 609 | var err error 610 | ec := EnumConstantElement{Name: label, Documentation: documentation} 611 | 612 | if ec.Tag, err = p.readInt(); err != nil { 613 | return p.errline("Unable to read tag for Enum Constant: %v due to: %v", label, err.Error()) 614 | } 615 | 616 | // If semicolon is next; we are done. If '[' is next, we must parse options for the enum constant 617 | if ec.Options, err = p.readListOptionsOnALine(); err != nil { 618 | return err 619 | } 620 | 621 | ee := ctx.obj.(*EnumElement) 622 | ee.EnumConstants = append(ee.EnumConstants, ec) 623 | return nil 624 | } 625 | 626 | func (p *parser) readOneOf(pf *ProtoFile, documentation string, ctx parseCtx) error { 627 | p.skipWhitespace() 628 | name, _, err := p.readName() 629 | if err != nil { 630 | return err 631 | } 632 | 633 | oe := OneOfElement{Name: name, Documentation: documentation} 634 | 635 | p.skipWhitespace() 636 | if c := p.read(); c != '{' { 637 | return p.throw('{', c) 638 | } 639 | 640 | innerCtx := parseCtx{ctxType: oneOfCtx, obj: &oe} 641 | if err = p.readDeclarationsInLoop(pf, innerCtx); err != nil { 642 | return err 643 | } 644 | 645 | me := ctx.obj.(*MessageElement) 646 | me.OneOfs = append(me.OneOfs, oe) 647 | return nil 648 | } 649 | 650 | func (p *parser) readExtend(pf *ProtoFile, documentation string, ctx parseCtx) error { 651 | p.skipWhitespace() 652 | name, _, err := p.readName() 653 | if err != nil { 654 | return err 655 | } 656 | qualifiedName := name 657 | if !strings.Contains(name, ".") && p.prefix != "" { 658 | qualifiedName = p.prefix + name 659 | } 660 | ee := ExtendElement{Name: name, QualifiedName: qualifiedName, Documentation: documentation} 661 | 662 | p.skipWhitespace() 663 | if c := p.read(); c != '{' { 664 | return p.throw('{', c) 665 | } 666 | 667 | innerCtx := parseCtx{ctxType: extendCtx, obj: &ee} 668 | if err = p.readDeclarationsInLoop(pf, innerCtx); err != nil { 669 | return err 670 | } 671 | 672 | // add extend declaration to the proper parent... 673 | if ctx.ctxType == msgCtx { 674 | me := ctx.obj.(*MessageElement) 675 | me.ExtendDeclarations = append(me.ExtendDeclarations, ee) 676 | } else { 677 | pf.ExtendDeclarations = append(pf.ExtendDeclarations, ee) 678 | } 679 | return nil 680 | } 681 | 682 | func (p *parser) readRPC(pf *ProtoFile, se *ServiceElement, documentation string) error { 683 | p.skipWhitespace() 684 | name, _, err := p.readName() 685 | if err != nil { 686 | return err 687 | } 688 | p.skipWhitespace() 689 | if c := p.read(); c != '(' { 690 | return p.throw('(', c) 691 | } 692 | 693 | // var requestType, responseType NamedDataType 694 | rpc := RPCElement{Name: name, Documentation: documentation} 695 | 696 | // parse request type... 697 | if rpc.RequestType, err = p.readRequestResponseType(); err != nil { 698 | return err 699 | } 700 | 701 | if c := p.read(); c != ')' { 702 | return p.throw(')', c) 703 | } 704 | p.skipWhitespace() 705 | 706 | if keyword := p.readWord(); keyword != "returns" { 707 | return p.errline("Expected 'returns', but found: %v", keyword) 708 | } 709 | 710 | p.skipWhitespace() 711 | if c := p.read(); c != '(' { 712 | return p.throw('(', c) 713 | } 714 | 715 | // parse response type... 716 | if rpc.ResponseType, err = p.readRequestResponseType(); err != nil { 717 | return err 718 | } 719 | 720 | if c := p.read(); c != ')' { 721 | return p.throw(')', c) 722 | } 723 | p.skipWhitespace() 724 | 725 | c := p.read() 726 | if c == '{' { 727 | ctx := parseCtx{ctxType: rpcCtx, obj: &rpc} 728 | for { 729 | c2 := p.read() 730 | if c2 == '}' { 731 | break 732 | } 733 | p.unread() 734 | if p.eofReached { 735 | break 736 | } 737 | 738 | withinRPCBracketsDocumentation, err := p.readDocumentationIfFound() 739 | if err != nil { 740 | return err 741 | } 742 | p.skipWhitespace() 743 | 744 | //parse for options... 745 | if err = p.readDeclaration(pf, withinRPCBracketsDocumentation, ctx); err != nil { 746 | return err 747 | } 748 | } 749 | } else if c != ';' { 750 | return p.throw(';', c) 751 | } 752 | 753 | se.RPCs = append(se.RPCs, rpc) 754 | return nil 755 | } 756 | 757 | func (p *parser) readService(pf *ProtoFile, documentation string) error { 758 | p.skipWhitespace() 759 | name, _, err := p.readName() 760 | if err != nil { 761 | return err 762 | } 763 | p.skipWhitespace() 764 | if c := p.read(); c != '{' { 765 | return p.throw('{', c) 766 | } 767 | 768 | se := ServiceElement{Name: name, QualifiedName: p.prefix + name, Documentation: documentation} 769 | 770 | ctx := parseCtx{ctxType: serviceCtx, obj: &se} 771 | if err = p.readDeclarationsInLoop(pf, ctx); err != nil { 772 | return err 773 | } 774 | 775 | pf.Services = append(pf.Services, se) 776 | return nil 777 | } 778 | 779 | func (p *parser) readEnum(pf *ProtoFile, documentation string, ctx parseCtx) error { 780 | p.skipWhitespace() 781 | name, _, err := p.readName() 782 | if err != nil { 783 | return err 784 | } 785 | p.skipWhitespace() 786 | if c := p.read(); c != '{' { 787 | return p.throw('{', c) 788 | } 789 | 790 | ee := EnumElement{Name: name, QualifiedName: p.prefix + name, Documentation: documentation} 791 | innerCtx := parseCtx{ctxType: enumCtx, obj: &ee} 792 | if err = p.readDeclarationsInLoop(pf, innerCtx); err != nil { 793 | return err 794 | } 795 | 796 | // add enum to the proper parent... 797 | if ctx.ctxType == msgCtx { 798 | me := ctx.obj.(*MessageElement) 799 | me.Enums = append(me.Enums, ee) 800 | } else { 801 | pf.Enums = append(pf.Enums, ee) 802 | } 803 | return nil 804 | } 805 | 806 | func (p *parser) readImport(pf *ProtoFile) error { 807 | // Define special matching function to match file path separator char 808 | f := func(r rune) bool { 809 | return r == '/' 810 | } 811 | 812 | p.skipWhitespace() 813 | c := p.read() 814 | p.unread() 815 | if c == '"' { 816 | importString, err := p.readQuotedString(f) 817 | if err != nil { 818 | return err 819 | } 820 | pf.Dependencies = append(pf.Dependencies, importString) 821 | } else { 822 | publicStr := p.readWord() 823 | if "public" != publicStr { 824 | return p.errline("Expected 'public', but found: %v", publicStr) 825 | } 826 | p.skipWhitespace() 827 | importString, err := p.readQuotedString(f) 828 | if err != nil { 829 | return err 830 | } 831 | pf.PublicDependencies = append(pf.PublicDependencies, importString) 832 | } 833 | if c := p.read(); c != ';' { 834 | return p.throw(';', c) 835 | } 836 | return nil 837 | } 838 | 839 | func (p *parser) readSyntax(pf *ProtoFile) error { 840 | p.skipWhitespace() 841 | if c := p.read(); c != '=' { 842 | return p.throw('=', c) 843 | } 844 | p.skipWhitespace() 845 | syntax, err := p.readQuotedString(nil) 846 | if err != nil { 847 | return err 848 | } 849 | if syntax != "proto2" && syntax != proto3 { 850 | return p.errline("'syntax' must be 'proto2' or 'proto3'. Found: %v", syntax) 851 | } 852 | if c := p.read(); c != ';' { 853 | return p.throw(';', c) 854 | } 855 | pf.Syntax = syntax 856 | return nil 857 | } 858 | 859 | func (p *parser) readQuotedString(f func(r rune) bool) (string, error) { 860 | if c := p.read(); c != '"' { 861 | return "", p.throw('"', c) 862 | } 863 | str := p.readWordAdvanced(f) 864 | if c := p.read(); c != '"' { 865 | return "", p.throw('"', c) 866 | } 867 | return str, nil 868 | } 869 | 870 | func (p *parser) readRequestResponseType() (NamedDataType, error) { 871 | name := p.readWord() 872 | 873 | // check for 'stream' keyword... 874 | var requiresStreaming bool 875 | if name == "stream" { 876 | requiresStreaming = true 877 | // get the actual data type 878 | p.skipWhitespace() 879 | name = p.readWord() 880 | } 881 | p.skipWhitespace() 882 | 883 | dt, err := p.readDataTypeInternal(name) 884 | switch t := dt.(type) { 885 | case NamedDataType: 886 | _ = t 887 | ndt := dt.(NamedDataType) 888 | ndt.stream(requiresStreaming) 889 | return ndt, err 890 | default: 891 | return NamedDataType{}, errors.New("Expected message type") 892 | } 893 | } 894 | 895 | func (p *parser) readDataType() (DataType, error) { 896 | name := p.readWord() 897 | p.skipWhitespace() 898 | return p.readDataTypeInternal(name) 899 | } 900 | 901 | func (p *parser) readDataTypeInternal(name string) (DataType, error) { 902 | // is it a map type? 903 | if name == "map" { 904 | if c := p.read(); c != '<' { 905 | return nil, p.throw('<', c) 906 | } 907 | var err error 908 | var keyType, valueType DataType 909 | keyType, err = p.readDataType() 910 | if err != nil { 911 | return nil, err 912 | } 913 | if c := p.read(); c != ',' { 914 | return nil, p.throw(',', c) 915 | } 916 | p.skipWhitespace() 917 | valueType, err = p.readDataType() 918 | if err != nil { 919 | return nil, err 920 | } 921 | if c := p.read(); c != '>' { 922 | return nil, p.throw('>', c) 923 | } 924 | return MapDataType{keyType: keyType, valueType: valueType}, nil 925 | } 926 | 927 | // is it a scalar type? 928 | sdt, err := NewScalarDataType(name) 929 | if err == nil { 930 | return sdt, nil 931 | } 932 | 933 | // must be a named type 934 | return NamedDataType{name: name}, nil 935 | } 936 | 937 | func (p *parser) unexpected(label string, ctx parseCtx) error { 938 | return p.errline("Unexpected '%v' in context: %v", label, ctx) 939 | } 940 | 941 | func (p *parser) throw(expected rune, actual rune) error { 942 | return p.errcol("Expected %v, but found: %v", strconv.QuoteRune(expected), strconv.QuoteRune(actual)) 943 | } 944 | 945 | func (p *parser) errline(msg string, a ...interface{}) error { 946 | s := fmt.Sprintf(msg, a...) 947 | return fmt.Errorf(s+" on line: %v", p.loc.line) 948 | } 949 | 950 | func (p *parser) errcol(msg string, a ...interface{}) error { 951 | s := fmt.Sprintf(msg, a...) 952 | return fmt.Errorf(s+" on line: %v, column: %v", p.loc.line, p.loc.column) 953 | } 954 | 955 | func (p *parser) readName() (string, enclosure, error) { 956 | var name string 957 | enc := unenclosed 958 | c := p.read() 959 | if c == '(' { 960 | enc = parenthesis 961 | name = p.readWord() 962 | if p.read() != ')' { 963 | return "", enc, p.errline("Expected ')'") 964 | } 965 | p.unread() 966 | } else if c == '[' { 967 | enc = bracket 968 | name = p.readWord() 969 | if p.read() != ']' { 970 | return "", enc, p.errline("Expected ']'") 971 | } 972 | p.unread() 973 | } else { 974 | p.unread() 975 | name = p.readWord() 976 | } 977 | return name, enc, nil 978 | } 979 | 980 | func (p *parser) readWord() string { 981 | return p.readWordAdvanced(nil) 982 | } 983 | 984 | func (p *parser) readWordAdvanced(f func(r rune) bool) string { 985 | var buf bytes.Buffer 986 | for { 987 | c := p.read() 988 | if isValidCharInWord(c, f) { 989 | _, _ = buf.WriteRune(c) 990 | } else { 991 | p.unread() 992 | break 993 | } 994 | } 995 | return buf.String() 996 | } 997 | 998 | func (p *parser) readInt() (int, error) { 999 | var buf bytes.Buffer 1000 | for { 1001 | c := p.read() 1002 | if isDigit(c) { 1003 | _, _ = buf.WriteRune(c) 1004 | } else { 1005 | p.unread() 1006 | break 1007 | } 1008 | } 1009 | str := buf.String() 1010 | intVal, err := strconv.Atoi(str) 1011 | return intVal, err 1012 | } 1013 | 1014 | func (p *parser) readDocumentation() (string, error) { 1015 | c := p.read() 1016 | if c == '/' { 1017 | return p.readSingleLineComment(), nil 1018 | } else if c == '*' { 1019 | return p.readMultiLineComment(), nil 1020 | } 1021 | return "", p.errline("Expected '/' or '*', but found: %v", strconv.QuoteRune(c)) 1022 | } 1023 | 1024 | func (p *parser) readMultiLineComment() string { 1025 | var buf bytes.Buffer 1026 | for { 1027 | c := p.read() 1028 | if c != '*' { 1029 | _, _ = buf.WriteRune(c) 1030 | } else { 1031 | c2 := p.read() 1032 | if c2 == '/' { 1033 | break 1034 | } 1035 | _, _ = buf.WriteRune(c2) 1036 | } 1037 | } 1038 | str := buf.String() 1039 | return strings.TrimSpace(str) 1040 | } 1041 | 1042 | // Reads one or multiple single line comments 1043 | func (p *parser) readSingleLineComment() string { 1044 | str := strings.TrimSpace(p.readUntilNewline()) 1045 | for { 1046 | p.skipWhitespace() 1047 | if c := p.read(); c != '/' { 1048 | p.unread() 1049 | break 1050 | } 1051 | if c := p.read(); c != '/' { 1052 | p.unread() 1053 | break 1054 | } 1055 | str += " " + strings.TrimSpace(p.readUntilNewline()) 1056 | } 1057 | return str 1058 | } 1059 | 1060 | func (p *parser) readUntil(delimiter byte) string { 1061 | s, err := p.br.ReadString(delimiter) 1062 | if err == io.EOF { 1063 | p.eofReached = true 1064 | } 1065 | return strings.TrimSuffix(s, string(delimiter)) 1066 | } 1067 | 1068 | func (p *parser) readUntilNewline() string { 1069 | return p.readUntil('\n') 1070 | } 1071 | 1072 | func (p *parser) skipUntilNewline() { 1073 | for { 1074 | c := p.read() 1075 | if c == '\n' { 1076 | return 1077 | } 1078 | if c == eof { 1079 | p.eofReached = true 1080 | return 1081 | } 1082 | } 1083 | } 1084 | 1085 | func (p *parser) unread() { 1086 | if p.loc.column == 0 { 1087 | p.loc.line-- 1088 | p.loc.column = p.lastColumnRead 1089 | } 1090 | _ = p.br.UnreadRune() 1091 | } 1092 | 1093 | func (p *parser) read() rune { 1094 | c, _, err := p.br.ReadRune() 1095 | if err != nil { 1096 | return eof 1097 | } 1098 | 1099 | p.lastColumnRead = p.loc.column 1100 | 1101 | if c == '\n' { 1102 | p.loc.line++ 1103 | p.loc.column = 0 1104 | } else { 1105 | p.loc.column++ 1106 | } 1107 | return c 1108 | } 1109 | 1110 | func (p *parser) skipWhitespace() { 1111 | for { 1112 | c := p.read() 1113 | if c == eof { 1114 | p.eofReached = true 1115 | break 1116 | } else if !isWhitespace(c) { 1117 | p.unread() 1118 | break 1119 | } 1120 | } 1121 | } 1122 | 1123 | func stripParenthesis(s string) (string, bool) { 1124 | if s[0] == '(' && s[len(s)-1] == ')' { 1125 | return parenthesisRemovalRegex.ReplaceAllString(s, "${1}"), true 1126 | } 1127 | return s, false 1128 | } 1129 | 1130 | func stripQuotes(s string) string { 1131 | if s[0] == '"' && s[len(s)-1] == '"' { 1132 | return quoteRemovalRegex.ReplaceAllString(s, "${1}") 1133 | } 1134 | return s 1135 | } 1136 | 1137 | func isValidCharInWord(c rune, f func(r rune) bool) bool { 1138 | if isLetter(c) || isDigit(c) || c == '_' || c == '-' || c == '.' { 1139 | return true 1140 | } else if f != nil { 1141 | return f(c) 1142 | } 1143 | return false 1144 | } 1145 | 1146 | func isStartOfComment(c rune) bool { 1147 | return c == '/' 1148 | } 1149 | 1150 | func isWhitespace(c rune) bool { 1151 | return c == ' ' || c == '\t' || c == '\r' || c == '\n' 1152 | } 1153 | 1154 | func isLetter(c rune) bool { 1155 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') 1156 | } 1157 | 1158 | func isDigit(c rune) bool { 1159 | return (c >= '0' && c <= '9') 1160 | } 1161 | 1162 | // End of the file... 1163 | var eof = rune(0) 1164 | 1165 | // Regex for removing bounding quotes 1166 | var quoteRemovalRegex = regexp.MustCompile(`"([^"]*)"`) 1167 | 1168 | // Regex for removing bounding parenthesis 1169 | var parenthesisRemovalRegex = regexp.MustCompile(`\(([^"]*)\)`) 1170 | 1171 | // enclousure used to bound/enclose a string 1172 | type enclosure int 1173 | 1174 | // enclosure the type of enclosures 1175 | const ( 1176 | parenthesis enclosure = iota 1177 | bracket 1178 | unenclosed 1179 | ) 1180 | 1181 | // some often-used string constants 1182 | const ( 1183 | proto3 = "proto3" 1184 | optional = "optional" 1185 | required = "required" 1186 | repeated = "repeated" 1187 | ) 1188 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package pbparser_test 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/tallstoat/pbparser" 9 | ) 10 | 11 | const ( 12 | errResourceDir string = "./resources/erroneous/" 13 | ) 14 | 15 | // NOTE: Keeping this reference around for benchmarking purposes 16 | var result pbparser.ProtoFile 17 | 18 | // BenchmarkParseFile benchmarks the ParseFile() API for a given .proto file. 19 | // This is meant to be used to uncover any hotspots or memory leaks or code which 20 | // can be optimized. 21 | func BenchmarkParseFile(b *testing.B) { 22 | b.ReportAllocs() 23 | const file = "./resources/descriptor.proto" 24 | 25 | var ( 26 | err error 27 | pf pbparser.ProtoFile 28 | ) 29 | 30 | for i := 1; i <= b.N; i++ { 31 | if pf, err = pbparser.ParseFile(file); err != nil { 32 | b.Errorf("%v", err.Error()) 33 | continue 34 | } 35 | } 36 | 37 | result = pf 38 | } 39 | 40 | // TestParseErrors is a test which is meant to cover most of the exception coditions 41 | // that the parser needs to catch. As such, this needs to be updated whenever new validations 42 | // are added in the parser or old validations are changed. Thus, this test ensures that the code 43 | // is in sync with the err identification expectations which are presented by the various proto 44 | // files it uses. 45 | func TestParseErrors(t *testing.T) { 46 | var tests = []struct { 47 | file string 48 | expectedErrors []string 49 | }{ 50 | {file: "missing-bracket-enum.proto", expectedErrors: []string{"Reached end of input in enum", "missing '}'"}}, 51 | {file: "missing-bracket-msg.proto", expectedErrors: []string{"Reached end of input in message", "missing '}'"}}, 52 | {file: "no-syntax.proto", expectedErrors: []string{"No syntax specified"}}, 53 | {file: "wrong-syntax.proto", expectedErrors: []string{"'syntax' must be 'proto2' or 'proto3'"}}, 54 | {file: "wrong-syntax2.proto", expectedErrors: []string{"Expected ';'"}}, 55 | {file: "wrong-syntax3.proto", expectedErrors: []string{"Expected '='"}}, 56 | {file: "optional-in-proto3.proto", expectedErrors: []string{"Explicit 'optional' labels are disallowed in the proto3 syntax"}}, 57 | {file: "required-in-proto3.proto", expectedErrors: []string{"Required fields are not allowed in proto3"}}, 58 | {file: "rpc-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'rpc' in context"}}, 59 | {file: "dup-enum.proto", expectedErrors: []string{"Duplicate name"}}, 60 | {file: "dup-enum-constant.proto", expectedErrors: []string{"Enum constant", "is already defined in package missing"}}, 61 | {file: "enum-constant-same-tag.proto", expectedErrors: []string{"is reusing an enum value. If this is intended, set 'option allow_alias = true;'"}}, 62 | {file: "wrong-enum-constant-tag.proto", expectedErrors: []string{"Unable to read tag for Enum Constant: UNKNOWN"}}, 63 | {file: "wrong-msg.proto", expectedErrors: []string{"Expected '{'"}}, 64 | {file: "dup-msg.proto", expectedErrors: []string{"Duplicate name"}}, 65 | {file: "dup-nested-msg.proto", expectedErrors: []string{"Duplicate name"}}, 66 | {file: "missing-msg.proto", expectedErrors: []string{"Datatype: 'TaskDetails' referenced in field: 'details' is not defined"}}, 67 | {file: "missing-package.proto", expectedErrors: []string{"Datatype: 'abcd.TaskDetails' referenced in field: 'details' is not defined"}}, 68 | {file: "wrong-import.proto", expectedErrors: []string{"ImportModuleReader is unable to provide content of dependency module"}}, 69 | {file: "wrong-import2.proto", expectedErrors: []string{"Expected 'public'"}}, 70 | {file: "wrong-import3.proto", expectedErrors: []string{"Expected '\"'"}}, 71 | {file: "wrong-public-import.proto", expectedErrors: []string{"ImportModuleReader is unable to provide content of dependency module"}}, 72 | {file: "wrong-rpc-datatype.proto", expectedErrors: []string{"Datatype: 'TaskId' referenced in RPC: 'AddTask' of Service: 'LogTask' is not defined"}}, 73 | {file: "wrong-label-in-oneof-field.proto", expectedErrors: []string{"Label 'repeated' is disallowed in oneoff field"}}, 74 | {file: "wrong-map-labels.proto", expectedErrors: []string{"Label required is not allowed on map fields"}}, 75 | {file: "wrong-map-declaration.proto", expectedErrors: []string{"Expected ',', but found: '>'"}}, 76 | {file: "wrong-map-in-oneof.proto", expectedErrors: []string{"Map fields are not allowed in oneofs"}}, 77 | {file: "wrong-map-key.proto", expectedErrors: []string{"Key in map fields cannot be float, double or bytes"}}, 78 | {file: "wrong-map-key2.proto", expectedErrors: []string{"Key in map fields cannot be a named type"}}, 79 | {file: "wrong-field.proto", expectedErrors: []string{"Expected '=', but found: '!'"}}, 80 | {file: "wrong-option.proto", expectedErrors: []string{"Expected '=', but found: '!'"}}, 81 | {file: "wrong-option2.proto", expectedErrors: []string{"Expected ';'"}}, 82 | {file: "wrong-inline-option.proto", expectedErrors: []string{"Option", "is not specified as expected"}}, 83 | {file: "wrong-oneof.proto", expectedErrors: []string{"Expected '{'"}}, 84 | {file: "wrong-extend.proto", expectedErrors: []string{"Expected '{'"}}, 85 | {file: "wrong-service.proto", expectedErrors: []string{"Expected '{'"}}, 86 | {file: "wrong-rpc.proto", expectedErrors: []string{"Expected 'returns'"}}, 87 | {file: "wrong-rpc2.proto", expectedErrors: []string{"Expected ';'"}}, 88 | {file: "package-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'package' in context: message"}}, 89 | {file: "syntax-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'syntax' in context: message"}}, 90 | {file: "import-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'import' in context: message"}}, 91 | {file: "msg-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'message' in context: service"}}, 92 | {file: "enum-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'enum' in context: service"}}, 93 | {file: "extend-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'extend' in context: service"}}, 94 | {file: "oneof-in-wrong-context.proto", expectedErrors: []string{"Unexpected 'oneof' in context: service"}}, 95 | {file: "unused-import.proto", expectedErrors: []string{"Imported package: dummy but not used"}}, 96 | } 97 | 98 | for _, tt := range tests { 99 | _, err := pbparser.ParseFile(errResourceDir + tt.file) 100 | if err != nil { 101 | for _, msg := range tt.expectedErrors { 102 | regex := regexp.MustCompile(msg) 103 | if !regex.MatchString(err.Error()) { 104 | t.Errorf("File: %v, ExpectedErr: [%v], ActualErr: [%v]\n", tt.file, msg, err.Error()) 105 | } 106 | } 107 | continue 108 | } 109 | } 110 | } 111 | 112 | // TestParseFile is a functional test which tests most success paths of the parser 113 | // by way of parsing a set of proto files. The proto files being used all conform to 114 | // the protobuf spec. This test also serves as a regression test which can be quickly 115 | // run post code changes to catch any regressions introduced. 116 | // 117 | // TODO: This is not an ideal test; needs assertions 118 | func TestParseFile(t *testing.T) { 119 | var tests = []struct { 120 | file string 121 | }{ 122 | {file: "./resources/enum.proto"}, 123 | {file: "./resources/service.proto"}, 124 | {file: "./resources/descriptor.proto"}, 125 | {file: "./resources/dep/dependent.proto"}, 126 | {file: "./resources/dep/dependent2.proto"}, 127 | } 128 | 129 | for i, tt := range tests { 130 | fmt.Printf("Running test: %v \n\n", i) 131 | 132 | fmt.Printf("Parsing file: %v \n", tt.file) 133 | pf, err := pbparser.ParseFile(tt.file) 134 | if err != nil { 135 | t.Errorf("%v", err.Error()) 136 | continue 137 | } 138 | 139 | fmt.Println("Syntax: " + pf.Syntax) 140 | fmt.Println("PackageName: " + pf.PackageName) 141 | for _, d := range pf.Dependencies { 142 | fmt.Println("Dependency: " + d) 143 | } 144 | for _, d := range pf.PublicDependencies { 145 | fmt.Println("PublicDependency: " + d) 146 | } 147 | options(pf.Options, "") 148 | 149 | for _, m := range pf.Messages { 150 | printMessage(&m, "") 151 | } 152 | 153 | for _, ed := range pf.ExtendDeclarations { 154 | fmt.Println("Extend: " + ed.Name) 155 | fmt.Println("QualifiedName: " + ed.QualifiedName) 156 | doc(ed.Documentation, "") 157 | fields(ed.Fields, tab) 158 | } 159 | 160 | for _, s := range pf.Services { 161 | fmt.Println("Service: " + s.Name) 162 | fmt.Println("QualifiedName: " + s.QualifiedName) 163 | doc(s.Documentation, "") 164 | options(s.Options, "") 165 | for _, rpc := range s.RPCs { 166 | printRPC(&rpc) 167 | } 168 | } 169 | 170 | for _, en := range pf.Enums { 171 | printEnum(&en, "") 172 | } 173 | } 174 | } 175 | 176 | func printMessage(m *pbparser.MessageElement, prefix string) { 177 | fmt.Println(prefix + "Message: " + m.Name) 178 | fmt.Println(prefix + "QualifiedName: " + m.QualifiedName) 179 | doc(m.Documentation, prefix) 180 | options(m.Options, prefix) 181 | fields(m.Fields, prefix+tab) 182 | for _, oo := range m.OneOfs { 183 | fmt.Println(prefix + tab + "OneOff: " + oo.Name) 184 | doc(oo.Documentation, prefix+tab) 185 | options(oo.Options, prefix+tab) 186 | fields(oo.Fields, prefix+tab2) 187 | } 188 | for _, xe := range m.Extensions { 189 | fmt.Printf("%vExtensions:: Start: %v End: %v\n", prefix+tab, xe.Start, xe.End) 190 | doc(xe.Documentation, prefix+tab) 191 | } 192 | for _, rn := range m.ReservedNames { 193 | fmt.Println(prefix + tab + "Reserved Name: " + rn) 194 | } 195 | for _, rr := range m.ReservedRanges { 196 | fmt.Printf("%vReserved Range:: Start: %v to End: %v\n", prefix+tab, rr.Start, rr.End) 197 | doc(rr.Documentation, prefix+tab) 198 | } 199 | for _, en := range m.Enums { 200 | printEnum(&en, prefix+tab) 201 | } 202 | for _, ed := range m.ExtendDeclarations { 203 | fmt.Println(prefix + "Extend: " + ed.Name) 204 | fmt.Println(prefix + "QualifiedName: " + ed.QualifiedName) 205 | doc(ed.Documentation, prefix) 206 | fields(ed.Fields, prefix+tab) 207 | } 208 | for _, nestedMsg := range m.Messages { 209 | printMessage(&nestedMsg, prefix+tab) 210 | } 211 | } 212 | 213 | func printRPC(rpc *pbparser.RPCElement) { 214 | fmt.Println(tab + "RPC: " + rpc.Name) 215 | doc(rpc.Documentation, tab) 216 | if rpc.RequestType.IsStream() { 217 | fmt.Println(tab + "RequestType: stream " + rpc.RequestType.Name()) 218 | } else { 219 | fmt.Println(tab + "RequestType: " + rpc.RequestType.Name()) 220 | } 221 | if rpc.ResponseType.IsStream() { 222 | fmt.Println(tab + "ResponseType: stream " + rpc.ResponseType.Name()) 223 | } else { 224 | fmt.Println(tab + "ResponseType: " + rpc.ResponseType.Name()) 225 | } 226 | options(rpc.Options, tab) 227 | } 228 | 229 | func printEnum(en *pbparser.EnumElement, prefix string) { 230 | fmt.Println(prefix + "Enum: " + en.Name) 231 | fmt.Println(prefix + "QualifiedName: " + en.QualifiedName) 232 | doc(en.Documentation, prefix) 233 | options(en.Options, prefix) 234 | for _, enc := range en.EnumConstants { 235 | fmt.Printf("%vName: %v Tag: %v\n", prefix+tab, enc.Name, enc.Tag) 236 | doc(enc.Documentation, prefix+tab) 237 | options(enc.Options, prefix+tab2) 238 | } 239 | } 240 | 241 | func options(options []pbparser.OptionElement, tab string) { 242 | for _, op := range options { 243 | if op.IsParenthesized { 244 | fmt.Printf("%vOption:: (%v) = %v\n", tab, op.Name, op.Value) 245 | } else { 246 | fmt.Printf("%vOption:: %v = %v\n", tab, op.Name, op.Value) 247 | } 248 | } 249 | } 250 | 251 | func fields(fields []pbparser.FieldElement, tab string) { 252 | for _, f := range fields { 253 | fmt.Println(tab + "Field: " + f.Name) 254 | if f.Label != "" { 255 | fmt.Println(tab + "Label: " + f.Label) 256 | } 257 | fmt.Printf("%vType: %v\n", tab, f.Type.Name()) 258 | fmt.Printf("%vTag: %v\n", tab, f.Tag) 259 | doc(f.Documentation, tab) 260 | options(f.Options, tab+tab) 261 | } 262 | } 263 | 264 | func doc(s string, tab string) { 265 | if s != "" { 266 | fmt.Println(tab + "Doc: " + s) 267 | } 268 | } 269 | 270 | func indent(i int) string { 271 | s := " " 272 | for j := 0; j < i; j++ { 273 | s += " " 274 | } 275 | return s 276 | } 277 | 278 | // init the tabs... 279 | var ( 280 | tab = indent(2) 281 | tab2 = indent(4) 282 | ) 283 | -------------------------------------------------------------------------------- /release-it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fail the script on error 4 | set -e 5 | 6 | read -p "Semantic version (without the 'v'): " VERSION 7 | 8 | if [[ $VERSION == v* ]] 9 | then 10 | echo ERROR: Please specify the raw semantic version without a 'v' prefix eg. x.y.z 11 | exit 1 12 | fi 13 | 14 | 15 | REVISION=`git rev-parse --short HEAD` 16 | echo Starting to tag $REVISION as v$VERSION... 17 | 18 | git tag -a v$VERSION -m "Release v$VERSION" 19 | if [ $? -eq 1 ]; then 20 | echo "Failed to tag; exiting..." 21 | exit 1 22 | fi 23 | 24 | echo Finished! 25 | echo "NOTE: Remember to execute (when satisfied): git push origin v$VERSION" 26 | echo 27 | -------------------------------------------------------------------------------- /resources/dep/dependency.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dep; 3 | 4 | message SamePackageDependencyMessage { 5 | string field = 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/dep/dependency2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dep; 3 | 4 | message SamePackageDependencyMessage { 5 | string field = 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/dep/dependent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dep; 3 | 4 | import "dependency.proto"; 5 | 6 | message Dependent { 7 | SamePackageDependencyMessage field = 1; 8 | } 9 | -------------------------------------------------------------------------------- /resources/dep/dependent2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dep; 3 | 4 | import "dependency.proto"; 5 | 6 | message Dependent { 7 | dep.SamePackageDependencyMessage field = 1; 8 | } 9 | -------------------------------------------------------------------------------- /resources/descriptor.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | // Author: kenton@google.com (Kenton Varda) 32 | // Based on original Protocol Buffers design by 33 | // Sanjay Ghemawat, Jeff Dean, and others. 34 | // 35 | // The messages in this file describe the definitions found in .proto files. 36 | // A valid .proto file can be translated directly to a FileDescriptorProto 37 | // without any other information (e.g. without reading its imports). 38 | 39 | 40 | syntax = "proto2"; 41 | 42 | package google.protobuf; 43 | option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor"; 44 | option java_package = "com.google.protobuf"; 45 | option java_outer_classname = "DescriptorProtos"; 46 | option csharp_namespace = "Google.Protobuf.Reflection"; 47 | option objc_class_prefix = "GPB"; 48 | 49 | // descriptor.proto must be optimized for speed because reflection-based 50 | // algorithms don't work during bootstrapping. 51 | option optimize_for = SPEED; 52 | 53 | // The protocol compiler can output a FileDescriptorSet containing the .proto 54 | // files it parses. 55 | message FileDescriptorSet { 56 | repeated FileDescriptorProto file = 1; 57 | } 58 | 59 | // Describes a complete .proto file. 60 | message FileDescriptorProto { 61 | optional string name = 1; // file name, relative to root of source tree 62 | optional string package = 2; // e.g. "foo", "foo.bar", etc. 63 | 64 | // Names of files imported by this file. 65 | repeated string dependency = 3; 66 | // Indexes of the public imported files in the dependency list above. 67 | repeated int32 public_dependency = 10; 68 | // Indexes of the weak imported files in the dependency list. 69 | // For Google-internal migration only. Do not use. 70 | repeated int32 weak_dependency = 11; 71 | 72 | // All top-level definitions in this file. 73 | repeated DescriptorProto message_type = 4; 74 | repeated EnumDescriptorProto enum_type = 5; 75 | repeated ServiceDescriptorProto service = 6; 76 | repeated FieldDescriptorProto extension = 7; 77 | 78 | optional FileOptions options = 8; 79 | 80 | // This field contains optional information about the original source code. 81 | // You may safely remove this entire field without harming runtime 82 | // functionality of the descriptors -- the information is needed only by 83 | // development tools. 84 | optional SourceCodeInfo source_code_info = 9; 85 | 86 | // The syntax of the proto file. 87 | // The supported values are "proto2" and "proto3". 88 | optional string syntax = 12; 89 | } 90 | 91 | // Describes a message type. 92 | message DescriptorProto { 93 | optional string name = 1; 94 | 95 | repeated FieldDescriptorProto field = 2; 96 | repeated FieldDescriptorProto extension = 6; 97 | 98 | repeated DescriptorProto nested_type = 3; 99 | repeated EnumDescriptorProto enum_type = 4; 100 | 101 | message ExtensionRange { 102 | optional int32 start = 1; 103 | optional int32 end = 2; 104 | } 105 | repeated ExtensionRange extension_range = 5; 106 | 107 | repeated OneofDescriptorProto oneof_decl = 8; 108 | 109 | optional MessageOptions options = 7; 110 | 111 | // Range of reserved tag numbers. Reserved tag numbers may not be used by 112 | // fields or extension ranges in the same message. Reserved ranges may 113 | // not overlap. 114 | message ReservedRange { 115 | optional int32 start = 1; // Inclusive. 116 | optional int32 end = 2; // Exclusive. 117 | } 118 | repeated ReservedRange reserved_range = 9; 119 | // Reserved field names, which may not be used by fields in the same message. 120 | // A given name may only be reserved once. 121 | repeated string reserved_name = 10; 122 | } 123 | 124 | // Describes a field within a message. 125 | message FieldDescriptorProto { 126 | enum Type { 127 | // 0 is reserved for errors. 128 | // Order is weird for historical reasons. 129 | TYPE_DOUBLE = 1; 130 | TYPE_FLOAT = 2; 131 | // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if 132 | // negative values are likely. 133 | TYPE_INT64 = 3; 134 | TYPE_UINT64 = 4; 135 | // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if 136 | // negative values are likely. 137 | TYPE_INT32 = 5; 138 | TYPE_FIXED64 = 6; 139 | TYPE_FIXED32 = 7; 140 | TYPE_BOOL = 8; 141 | TYPE_STRING = 9; 142 | // Tag-delimited aggregate. 143 | // Group type is deprecated and not supported in proto3. However, Proto3 144 | // implementations should still be able to parse the group wire format and 145 | // treat group fields as unknown fields. 146 | TYPE_GROUP = 10; 147 | TYPE_MESSAGE = 11; // Length-delimited aggregate. 148 | 149 | // New in version 2. 150 | TYPE_BYTES = 12; 151 | TYPE_UINT32 = 13; 152 | TYPE_ENUM = 14; 153 | TYPE_SFIXED32 = 15; 154 | TYPE_SFIXED64 = 16; 155 | TYPE_SINT32 = 17; // Uses ZigZag encoding. 156 | TYPE_SINT64 = 18; // Uses ZigZag encoding. 157 | }; 158 | 159 | enum Label { 160 | // 0 is reserved for errors 161 | LABEL_OPTIONAL = 1; 162 | LABEL_REQUIRED = 2; 163 | LABEL_REPEATED = 3; 164 | }; 165 | 166 | optional string name = 1; 167 | optional int32 number = 3; 168 | optional Label label = 4; 169 | 170 | // If type_name is set, this need not be set. If both this and type_name 171 | // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. 172 | optional Type type = 5; 173 | 174 | // For message and enum types, this is the name of the type. If the name 175 | // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping 176 | // rules are used to find the type (i.e. first the nested types within this 177 | // message are searched, then within the parent, on up to the root 178 | // namespace). 179 | optional string type_name = 6; 180 | 181 | // For extensions, this is the name of the type being extended. It is 182 | // resolved in the same manner as type_name. 183 | optional string extendee = 2; 184 | 185 | // For numeric types, contains the original text representation of the value. 186 | // For booleans, "true" or "false". 187 | // For strings, contains the default text contents (not escaped in any way). 188 | // For bytes, contains the C escaped value. All bytes >= 128 are escaped. 189 | // TODO(kenton): Base-64 encode? 190 | optional string default_value = 7; 191 | 192 | // If set, gives the index of a oneof in the containing type's oneof_decl 193 | // list. This field is a member of that oneof. 194 | optional int32 oneof_index = 9; 195 | 196 | // JSON name of this field. The value is set by protocol compiler. If the 197 | // user has set a "json_name" option on this field, that option's value 198 | // will be used. Otherwise, it's deduced from the field's name by converting 199 | // it to camelCase. 200 | optional string json_name = 10; 201 | 202 | optional FieldOptions options = 8; 203 | } 204 | 205 | // Describes a oneof. 206 | message OneofDescriptorProto { 207 | optional string name = 1; 208 | optional OneofOptions options = 2; 209 | } 210 | 211 | // Describes an enum type. 212 | message EnumDescriptorProto { 213 | optional string name = 1; 214 | 215 | repeated EnumValueDescriptorProto value = 2; 216 | 217 | optional EnumOptions options = 3; 218 | } 219 | 220 | // Describes a value within an enum. 221 | message EnumValueDescriptorProto { 222 | optional string name = 1; 223 | optional int32 number = 2; 224 | 225 | optional EnumValueOptions options = 3; 226 | } 227 | 228 | // Describes a service. 229 | message ServiceDescriptorProto { 230 | optional string name = 1; 231 | repeated MethodDescriptorProto method = 2; 232 | 233 | optional ServiceOptions options = 3; 234 | } 235 | 236 | // Describes a method of a service. 237 | message MethodDescriptorProto { 238 | optional string name = 1; 239 | 240 | // Input and output type names. These are resolved in the same way as 241 | // FieldDescriptorProto.type_name, but must refer to a message type. 242 | optional string input_type = 2; 243 | optional string output_type = 3; 244 | 245 | optional MethodOptions options = 4; 246 | 247 | // Identifies if client streams multiple client messages 248 | optional bool client_streaming = 5 [default=false]; 249 | // Identifies if server streams multiple server messages 250 | optional bool server_streaming = 6 [default=false]; 251 | } 252 | 253 | 254 | // =================================================================== 255 | // Options 256 | 257 | // Each of the definitions above may have "options" attached. These are 258 | // just annotations which may cause code to be generated slightly differently 259 | // or may contain hints for code that manipulates protocol messages. 260 | // 261 | // Clients may define custom options as extensions of the *Options messages. 262 | // These extensions may not yet be known at parsing time, so the parser cannot 263 | // store the values in them. Instead it stores them in a field in the *Options 264 | // message called uninterpreted_option. This field must have the same name 265 | // across all *Options messages. We then use this field to populate the 266 | // extensions when we build a descriptor, at which point all protos have been 267 | // parsed and so all extensions are known. 268 | // 269 | // Extension numbers for custom options may be chosen as follows: 270 | // * For options which will only be used within a single application or 271 | // organization, or for experimental options, use field numbers 50000 272 | // through 99999. It is up to you to ensure that you do not use the 273 | // same number for multiple options. 274 | // * For options which will be published and used publicly by multiple 275 | // independent entities, e-mail protobuf-global-extension-registry@google.com 276 | // to reserve extension numbers. Simply provide your project name (e.g. 277 | // Objective-C plugin) and your project website (if available) -- there's no 278 | // need to explain how you intend to use them. Usually you only need one 279 | // extension number. You can declare multiple options with only one extension 280 | // number by putting them in a sub-message. See the Custom Options section of 281 | // the docs for examples: 282 | // https://developers.google.com/protocol-buffers/docs/proto#options 283 | // If this turns out to be popular, a web service will be set up 284 | // to automatically assign option numbers. 285 | 286 | 287 | message FileOptions { 288 | 289 | // Sets the Java package where classes generated from this .proto will be 290 | // placed. By default, the proto package is used, but this is often 291 | // inappropriate because proto packages do not normally start with backwards 292 | // domain names. 293 | optional string java_package = 1; 294 | 295 | 296 | // If set, all the classes from the .proto file are wrapped in a single 297 | // outer class with the given name. This applies to both Proto1 298 | // (equivalent to the old "--one_java_file" option) and Proto2 (where 299 | // a .proto always translates to a single class, but you may want to 300 | // explicitly choose the class name). 301 | optional string java_outer_classname = 8; 302 | 303 | // If set true, then the Java code generator will generate a separate .java 304 | // file for each top-level message, enum, and service defined in the .proto 305 | // file. Thus, these types will *not* be nested inside the outer class 306 | // named by java_outer_classname. However, the outer class will still be 307 | // generated to contain the file's getDescriptor() method as well as any 308 | // top-level extensions defined in the file. 309 | optional bool java_multiple_files = 10 [default=false]; 310 | 311 | // This option does nothing. 312 | optional bool java_generate_equals_and_hash = 20 [deprecated=true]; 313 | 314 | // If set true, then the Java2 code generator will generate code that 315 | // throws an exception whenever an attempt is made to assign a non-UTF-8 316 | // byte sequence to a string field. 317 | // Message reflection will do the same. 318 | // However, an extension field still accepts non-UTF-8 byte sequences. 319 | // This option has no effect on when used with the lite runtime. 320 | optional bool java_string_check_utf8 = 27 [default=false]; 321 | 322 | 323 | // Generated classes can be optimized for speed or code size. 324 | enum OptimizeMode { 325 | SPEED = 1; // Generate complete code for parsing, serialization, 326 | // etc. 327 | CODE_SIZE = 2; // Use ReflectionOps to implement these methods. 328 | LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. 329 | } 330 | optional OptimizeMode optimize_for = 9 [default=SPEED]; 331 | 332 | // Sets the Go package where structs generated from this .proto will be 333 | // placed. If omitted, the Go package will be derived from the following: 334 | // - The basename of the package import path, if provided. 335 | // - Otherwise, the package statement in the .proto file, if present. 336 | // - Otherwise, the basename of the .proto file, without extension. 337 | optional string go_package = 11; 338 | 339 | 340 | 341 | // Should generic services be generated in each language? "Generic" services 342 | // are not specific to any particular RPC system. They are generated by the 343 | // main code generators in each language (without additional plugins). 344 | // Generic services were the only kind of service generation supported by 345 | // early versions of google.protobuf. 346 | // 347 | // Generic services are now considered deprecated in favor of using plugins 348 | // that generate code specific to your particular RPC system. Therefore, 349 | // these default to false. Old code which depends on generic services should 350 | // explicitly set them to true. 351 | optional bool cc_generic_services = 16 [default=false]; 352 | optional bool java_generic_services = 17 [default=false]; 353 | optional bool py_generic_services = 18 [default=false]; 354 | 355 | // Is this file deprecated? 356 | // Depending on the target platform, this can emit Deprecated annotations 357 | // for everything in the file, or it will be completely ignored; in the very 358 | // least, this is a formalization for deprecating files. 359 | optional bool deprecated = 23 [default=false]; 360 | 361 | // Enables the use of arenas for the proto messages in this file. This applies 362 | // only to generated classes for C++. 363 | optional bool cc_enable_arenas = 31 [default=false]; 364 | 365 | 366 | // Sets the objective c class prefix which is prepended to all objective c 367 | // generated classes from this .proto. There is no default. 368 | optional string objc_class_prefix = 36; 369 | 370 | // Namespace for generated classes; defaults to the package. 371 | optional string csharp_namespace = 37; 372 | 373 | // By default Swift generators will take the proto package and CamelCase it 374 | // replacing '.' with underscore and use that to prefix the types/symbols 375 | // defined. When this options is provided, they will use this value instead 376 | // to prefix the types/symbols defined. 377 | optional string swift_prefix = 39; 378 | 379 | // Sets the php class prefix which is prepended to all php generated classes 380 | // from this .proto. Default is empty. 381 | optional string php_class_prefix = 40; 382 | 383 | // The parser stores options it doesn't recognize here. See above. 384 | repeated UninterpretedOption uninterpreted_option = 999; 385 | 386 | // Clients can define custom options in extensions of this message. See above. 387 | extensions 1000 to max; 388 | 389 | reserved 38; 390 | } 391 | 392 | message MessageOptions { 393 | // Set true to use the old proto1 MessageSet wire format for extensions. 394 | // This is provided for backwards-compatibility with the MessageSet wire 395 | // format. You should not use this for any other reason: It's less 396 | // efficient, has fewer features, and is more complicated. 397 | // 398 | // The message must be defined exactly as follows: 399 | // message Foo { 400 | // option message_set_wire_format = true; 401 | // extensions 4 to max; 402 | // } 403 | // Note that the message cannot have any defined fields; MessageSets only 404 | // have extensions. 405 | // 406 | // All extensions of your type must be singular messages; e.g. they cannot 407 | // be int32s, enums, or repeated messages. 408 | // 409 | // Because this is an option, the above two restrictions are not enforced by 410 | // the protocol compiler. 411 | optional bool message_set_wire_format = 1 [default=false]; 412 | 413 | // Disables the generation of the standard "descriptor()" accessor, which can 414 | // conflict with a field of the same name. This is meant to make migration 415 | // from proto1 easier; new code should avoid fields named "descriptor". 416 | optional bool no_standard_descriptor_accessor = 2 [default=false]; 417 | 418 | // Is this message deprecated? 419 | // Depending on the target platform, this can emit Deprecated annotations 420 | // for the message, or it will be completely ignored; in the very least, 421 | // this is a formalization for deprecating messages. 422 | optional bool deprecated = 3 [default=false]; 423 | 424 | // Whether the message is an automatically generated map entry type for the 425 | // maps field. 426 | // 427 | // For maps fields: 428 | // map map_field = 1; 429 | // The parsed descriptor looks like: 430 | // message MapFieldEntry { 431 | // option map_entry = true; 432 | // optional KeyType key = 1; 433 | // optional ValueType value = 2; 434 | // } 435 | // repeated MapFieldEntry map_field = 1; 436 | // 437 | // Implementations may choose not to generate the map_entry=true message, but 438 | // use a native map in the target language to hold the keys and values. 439 | // The reflection APIs in such implementions still need to work as 440 | // if the field is a repeated message field. 441 | // 442 | // NOTE: Do not set the option in .proto files. Always use the maps syntax 443 | // instead. The option should only be implicitly set by the proto compiler 444 | // parser. 445 | optional bool map_entry = 7; 446 | 447 | reserved 8; // javalite_serializable 448 | reserved 9; // javanano_as_lite 449 | 450 | // The parser stores options it doesn't recognize here. See above. 451 | repeated UninterpretedOption uninterpreted_option = 999; 452 | 453 | // Clients can define custom options in extensions of this message. See above. 454 | extensions 1000 to max; 455 | } 456 | 457 | message FieldOptions { 458 | // The ctype option instructs the C++ code generator to use a different 459 | // representation of the field than it normally would. See the specific 460 | // options below. This option is not yet implemented in the open source 461 | // release -- sorry, we'll try to include it in a future version! 462 | optional CType ctype = 1 [default = STRING]; 463 | enum CType { 464 | // Default mode. 465 | STRING = 0; 466 | 467 | CORD = 1; 468 | 469 | STRING_PIECE = 2; 470 | } 471 | // The packed option can be enabled for repeated primitive fields to enable 472 | // a more efficient representation on the wire. Rather than repeatedly 473 | // writing the tag and type for each element, the entire array is encoded as 474 | // a single length-delimited blob. In proto3, only explicit setting it to 475 | // false will avoid using packed encoding. 476 | optional bool packed = 2; 477 | 478 | // The jstype option determines the JavaScript type used for values of the 479 | // field. The option is permitted only for 64 bit integral and fixed types 480 | // (int64, uint64, sint64, fixed64, sfixed64). By default these types are 481 | // represented as JavaScript strings. This avoids loss of precision that can 482 | // happen when a large value is converted to a floating point JavaScript 483 | // numbers. Specifying JS_NUMBER for the jstype causes the generated 484 | // JavaScript code to use the JavaScript "number" type instead of strings. 485 | // This option is an enum to permit additional types to be added, 486 | // e.g. goog.math.Integer. 487 | optional JSType jstype = 6 [default = JS_NORMAL]; 488 | enum JSType { 489 | // Use the default type. 490 | JS_NORMAL = 0; 491 | 492 | // Use JavaScript strings. 493 | JS_STRING = 1; 494 | 495 | // Use JavaScript numbers. 496 | JS_NUMBER = 2; 497 | } 498 | 499 | // Should this field be parsed lazily? Lazy applies only to message-type 500 | // fields. It means that when the outer message is initially parsed, the 501 | // inner message's contents will not be parsed but instead stored in encoded 502 | // form. The inner message will actually be parsed when it is first accessed. 503 | // 504 | // This is only a hint. Implementations are free to choose whether to use 505 | // eager or lazy parsing regardless of the value of this option. However, 506 | // setting this option true suggests that the protocol author believes that 507 | // using lazy parsing on this field is worth the additional bookkeeping 508 | // overhead typically needed to implement it. 509 | // 510 | // This option does not affect the public interface of any generated code; 511 | // all method signatures remain the same. Furthermore, thread-safety of the 512 | // interface is not affected by this option; const methods remain safe to 513 | // call from multiple threads concurrently, while non-const methods continue 514 | // to require exclusive access. 515 | // 516 | // 517 | // Note that implementations may choose not to check required fields within 518 | // a lazy sub-message. That is, calling IsInitialized() on the outer message 519 | // may return true even if the inner message has missing required fields. 520 | // This is necessary because otherwise the inner message would have to be 521 | // parsed in order to perform the check, defeating the purpose of lazy 522 | // parsing. An implementation which chooses not to check required fields 523 | // must be consistent about it. That is, for any particular sub-message, the 524 | // implementation must either *always* check its required fields, or *never* 525 | // check its required fields, regardless of whether or not the message has 526 | // been parsed. 527 | optional bool lazy = 5 [default=false]; 528 | 529 | // Is this field deprecated? 530 | // Depending on the target platform, this can emit Deprecated annotations 531 | // for accessors, or it will be completely ignored; in the very least, this 532 | // is a formalization for deprecating fields. 533 | optional bool deprecated = 3 [default=false]; 534 | 535 | // For Google-internal migration only. Do not use. 536 | optional bool weak = 10 [default=false]; 537 | 538 | 539 | // The parser stores options it doesn't recognize here. See above. 540 | repeated UninterpretedOption uninterpreted_option = 999; 541 | 542 | // Clients can define custom options in extensions of this message. See above. 543 | extensions 1000 to max; 544 | 545 | reserved 4; // removed jtype 546 | } 547 | 548 | message OneofOptions { 549 | // The parser stores options it doesn't recognize here. See above. 550 | repeated UninterpretedOption uninterpreted_option = 999; 551 | 552 | // Clients can define custom options in extensions of this message. See above. 553 | extensions 1000 to max; 554 | } 555 | 556 | message EnumOptions { 557 | 558 | // Set this option to true to allow mapping different tag names to the same 559 | // value. 560 | optional bool allow_alias = 2; 561 | 562 | // Is this enum deprecated? 563 | // Depending on the target platform, this can emit Deprecated annotations 564 | // for the enum, or it will be completely ignored; in the very least, this 565 | // is a formalization for deprecating enums. 566 | optional bool deprecated = 3 [default=false]; 567 | 568 | reserved 5; // javanano_as_lite 569 | 570 | // The parser stores options it doesn't recognize here. See above. 571 | repeated UninterpretedOption uninterpreted_option = 999; 572 | 573 | // Clients can define custom options in extensions of this message. See above. 574 | extensions 1000 to max; 575 | } 576 | 577 | message EnumValueOptions { 578 | // Is this enum value deprecated? 579 | // Depending on the target platform, this can emit Deprecated annotations 580 | // for the enum value, or it will be completely ignored; in the very least, 581 | // this is a formalization for deprecating enum values. 582 | optional bool deprecated = 1 [default=false]; 583 | 584 | // The parser stores options it doesn't recognize here. See above. 585 | repeated UninterpretedOption uninterpreted_option = 999; 586 | 587 | // Clients can define custom options in extensions of this message. See above. 588 | extensions 1000 to max; 589 | } 590 | 591 | message ServiceOptions { 592 | 593 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 594 | // framework. We apologize for hoarding these numbers to ourselves, but 595 | // we were already using them long before we decided to release Protocol 596 | // Buffers. 597 | 598 | // Is this service deprecated? 599 | // Depending on the target platform, this can emit Deprecated annotations 600 | // for the service, or it will be completely ignored; in the very least, 601 | // this is a formalization for deprecating services. 602 | optional bool deprecated = 33 [default=false]; 603 | 604 | // The parser stores options it doesn't recognize here. See above. 605 | repeated UninterpretedOption uninterpreted_option = 999; 606 | 607 | // Clients can define custom options in extensions of this message. See above. 608 | extensions 1000 to max; 609 | } 610 | 611 | message MethodOptions { 612 | 613 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 614 | // framework. We apologize for hoarding these numbers to ourselves, but 615 | // we were already using them long before we decided to release Protocol 616 | // Buffers. 617 | 618 | // Is this method deprecated? 619 | // Depending on the target platform, this can emit Deprecated annotations 620 | // for the method, or it will be completely ignored; in the very least, 621 | // this is a formalization for deprecating methods. 622 | optional bool deprecated = 33 [default=false]; 623 | 624 | // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, 625 | // or neither? HTTP based RPC implementation may choose GET verb for safe 626 | // methods, and PUT verb for idempotent methods instead of the default POST. 627 | enum IdempotencyLevel { 628 | IDEMPOTENCY_UNKNOWN = 0; 629 | NO_SIDE_EFFECTS = 1; // implies idempotent 630 | IDEMPOTENT = 2; // idempotent, but may have side effects 631 | } 632 | optional IdempotencyLevel idempotency_level = 633 | 34 [default=IDEMPOTENCY_UNKNOWN]; 634 | 635 | // The parser stores options it doesn't recognize here. See above. 636 | repeated UninterpretedOption uninterpreted_option = 999; 637 | 638 | // Clients can define custom options in extensions of this message. See above. 639 | extensions 1000 to max; 640 | } 641 | 642 | 643 | // A message representing a option the parser does not recognize. This only 644 | // appears in options protos created by the compiler::Parser class. 645 | // DescriptorPool resolves these when building Descriptor objects. Therefore, 646 | // options protos in descriptor objects (e.g. returned by Descriptor::options(), 647 | // or produced by Descriptor::CopyTo()) will never have UninterpretedOptions 648 | // in them. 649 | message UninterpretedOption { 650 | // The name of the uninterpreted option. Each string represents a segment in 651 | // a dot-separated name. is_extension is true iff a segment represents an 652 | // extension (denoted with parentheses in options specs in .proto files). 653 | // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents 654 | // "foo.(bar.baz).qux". 655 | message NamePart { 656 | required string name_part = 1; 657 | required bool is_extension = 2; 658 | } 659 | repeated NamePart name = 2; 660 | 661 | // The value of the uninterpreted option, in whatever type the tokenizer 662 | // identified it as during parsing. Exactly one of these should be set. 663 | optional string identifier_value = 3; 664 | optional uint64 positive_int_value = 4; 665 | optional int64 negative_int_value = 5; 666 | optional double double_value = 6; 667 | optional bytes string_value = 7; 668 | optional string aggregate_value = 8; 669 | } 670 | 671 | // =================================================================== 672 | // Optional source code info 673 | 674 | // Encapsulates information about the original source file from which a 675 | // FileDescriptorProto was generated. 676 | message SourceCodeInfo { 677 | // A Location identifies a piece of source code in a .proto file which 678 | // corresponds to a particular definition. This information is intended 679 | // to be useful to IDEs, code indexers, documentation generators, and similar 680 | // tools. 681 | // 682 | // For example, say we have a file like: 683 | // message Foo { 684 | // optional string foo = 1; 685 | // } 686 | // Let's look at just the field definition: 687 | // optional string foo = 1; 688 | // ^ ^^ ^^ ^ ^^^ 689 | // a bc de f ghi 690 | // We have the following locations: 691 | // span path represents 692 | // [a,i) [ 4, 0, 2, 0 ] The whole field definition. 693 | // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). 694 | // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). 695 | // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). 696 | // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). 697 | // 698 | // Notes: 699 | // - A location may refer to a repeated field itself (i.e. not to any 700 | // particular index within it). This is used whenever a set of elements are 701 | // logically enclosed in a single code segment. For example, an entire 702 | // extend block (possibly containing multiple extension definitions) will 703 | // have an outer location whose path refers to the "extensions" repeated 704 | // field without an index. 705 | // - Multiple locations may have the same path. This happens when a single 706 | // logical declaration is spread out across multiple places. The most 707 | // obvious example is the "extend" block again -- there may be multiple 708 | // extend blocks in the same scope, each of which will have the same path. 709 | // - A location's span is not always a subset of its parent's span. For 710 | // example, the "extendee" of an extension declaration appears at the 711 | // beginning of the "extend" block and is shared by all extensions within 712 | // the block. 713 | // - Just because a location's span is a subset of some other location's span 714 | // does not mean that it is a descendent. For example, a "group" defines 715 | // both a type and a field in a single declaration. Thus, the locations 716 | // corresponding to the type and field and their components will overlap. 717 | // - Code which tries to interpret locations should probably be designed to 718 | // ignore those that it doesn't understand, as more types of locations could 719 | // be recorded in the future. 720 | repeated Location location = 1; 721 | message Location { 722 | // Identifies which part of the FileDescriptorProto was defined at this 723 | // location. 724 | // 725 | // Each element is a field number or an index. They form a path from 726 | // the root FileDescriptorProto to the place where the definition. For 727 | // example, this path: 728 | // [ 4, 3, 2, 7, 1 ] 729 | // refers to: 730 | // file.message_type(3) // 4, 3 731 | // .field(7) // 2, 7 732 | // .name() // 1 733 | // This is because FileDescriptorProto.message_type has field number 4: 734 | // repeated DescriptorProto message_type = 4; 735 | // and DescriptorProto.field has field number 2: 736 | // repeated FieldDescriptorProto field = 2; 737 | // and FieldDescriptorProto.name has field number 1: 738 | // optional string name = 1; 739 | // 740 | // Thus, the above path gives the location of a field name. If we removed 741 | // the last element: 742 | // [ 4, 3, 2, 7 ] 743 | // this path refers to the whole field declaration (from the beginning 744 | // of the label to the terminating semicolon). 745 | repeated int32 path = 1 [packed=true]; 746 | 747 | // Always has exactly three or four elements: start line, start column, 748 | // end line (optional, otherwise assumed same as start line), end column. 749 | // These are packed into a single field for efficiency. Note that line 750 | // and column numbers are zero-based -- typically you will want to add 751 | // 1 to each before displaying to a user. 752 | repeated int32 span = 2 [packed=true]; 753 | 754 | // If this SourceCodeInfo represents a complete declaration, these are any 755 | // comments appearing before and after the declaration which appear to be 756 | // attached to the declaration. 757 | // 758 | // A series of line comments appearing on consecutive lines, with no other 759 | // tokens appearing on those lines, will be treated as a single comment. 760 | // 761 | // leading_detached_comments will keep paragraphs of comments that appear 762 | // before (but not connected to) the current element. Each paragraph, 763 | // separated by empty lines, will be one comment element in the repeated 764 | // field. 765 | // 766 | // Only the comment content is provided; comment markers (e.g. //) are 767 | // stripped out. For block comments, leading whitespace and an asterisk 768 | // will be stripped from the beginning of each line other than the first. 769 | // Newlines are included in the output. 770 | // 771 | // Examples: 772 | // 773 | // optional int32 foo = 1; // Comment attached to foo. 774 | // // Comment attached to bar. 775 | // optional int32 bar = 2; 776 | // 777 | // optional string baz = 3; 778 | // // Comment attached to baz. 779 | // // Another line attached to baz. 780 | // 781 | // // Comment attached to qux. 782 | // // 783 | // // Another line attached to qux. 784 | // optional double qux = 4; 785 | // 786 | // // Detached comment for corge. This is not leading or trailing comments 787 | // // to qux or corge because there are blank lines separating it from 788 | // // both. 789 | // 790 | // // Detached comment for corge paragraph 2. 791 | // 792 | // optional string corge = 5; 793 | // /* Block comment attached 794 | // * to corge. Leading asterisks 795 | // * will be removed. */ 796 | // /* Block comment attached to 797 | // * grault. */ 798 | // optional int32 grault = 6; 799 | // 800 | // // ignored detached comments. 801 | optional string leading_comments = 3; 802 | optional string trailing_comments = 4; 803 | repeated string leading_detached_comments = 6; 804 | } 805 | } 806 | 807 | // Describes the relationship between generated code and its original source 808 | // file. A GeneratedCodeInfo message is associated with only one generated 809 | // source file, but may contain references to different source .proto files. 810 | message GeneratedCodeInfo { 811 | // An Annotation connects some span of text in generated code to an element 812 | // of its generating .proto file. 813 | repeated Annotation annotation = 1; 814 | message Annotation { 815 | // Identifies the element in the original source .proto file. This field 816 | // is formatted the same as SourceCodeInfo.Location.path. 817 | repeated int32 path = 1 [packed=true]; 818 | 819 | // Identifies the filesystem path to the original source .proto. 820 | optional string source_file = 2; 821 | 822 | // Identifies the starting offset in bytes in the generated code 823 | // that relates to the identified object. 824 | optional int32 begin = 3; 825 | 826 | // Identifies the ending offset in bytes in the generated code that 827 | // relates to the identified offset. The end offset should be one past 828 | // the last relevant byte (so the length of the text = end - begin). 829 | optional int32 end = 4; 830 | } 831 | } 832 | -------------------------------------------------------------------------------- /resources/enum.proto: -------------------------------------------------------------------------------- 1 | /* Task object blah blah adsd 2 | sfsf fafasf affaf fasfaf 3 | */ 4 | syntax = "proto3"; 5 | package enumpkg; 6 | 7 | // EnumAllowingAlias docs for testing... 8 | enum EnumAllowingAlias { 9 | option rah = true; 10 | UNKNOWN = 0; 11 | // da dada dum 12 | STARTED = 1 [gah = 3]; 13 | RUNNING = 2; 14 | } 15 | 16 | message Outer { 17 | message MiddleAA { 18 | message Inner { 19 | int64 ival = 1; 20 | bool booly = 2; 21 | } 22 | } 23 | message MiddleBB { 24 | message Inner { 25 | int32 ival = 1; 26 | bool booly = 2; 27 | message Deep { 28 | int32 xval = 1; 29 | enum Dowop { 30 | option allow_alias = true; 31 | UNKNOWN = 0; 32 | STARTING = 0; 33 | } 34 | enum Dowop2 { 35 | UNKNOWN2 = 0; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/erroneous/dummy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dummy; 3 | 4 | message Empty { 5 | } 6 | -------------------------------------------------------------------------------- /resources/erroneous/dup-enum-constant.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | UNKNOWN = 0; 6 | } 7 | 8 | enum EnumAllowingAlias2 { 9 | UNKNOWN = 0; 10 | } 11 | -------------------------------------------------------------------------------- /resources/erroneous/dup-enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | UNKNOWN = 0; 6 | RUNNING = 1; 7 | } 8 | 9 | enum EnumAllowingAlias { 10 | UNKNOWN2 = 0; 11 | RUNNING2 = 1; 12 | } 13 | -------------------------------------------------------------------------------- /resources/erroneous/dup-msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | } 7 | 8 | message Task { 9 | string name = 1; 10 | } 11 | -------------------------------------------------------------------------------- /resources/erroneous/dup-nested-msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | message Child { 7 | string desc = 1; 8 | } 9 | string name = 2; 10 | message Child { 11 | string label = 1; 12 | } 13 | } 14 | 15 | message Task { 16 | string name = 1; 17 | } 18 | -------------------------------------------------------------------------------- /resources/erroneous/enum-constant-same-tag.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | // EnumAllowingAlias docs for testing... 5 | enum EnumAllowingAlias { 6 | option rah = true; 7 | UNKNOWN = 0; 8 | // da dada dum 9 | STARTED = 1 [gah = 3]; 10 | RUNNING = 1; 11 | } 12 | -------------------------------------------------------------------------------- /resources/erroneous/enum-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | service TaskMaster { 5 | enum State { 6 | UNKNOWN = 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/erroneous/extend-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | service TaskMaster { 5 | extend State { 6 | string abc = 128; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/erroneous/import-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | import "abcd.proto" 6 | string id = 1; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/missing-bracket-enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | // EnumAllowingAlias docs for testing... 5 | enum EnumAllowingAlias { 6 | option rah = true; 7 | UNKNOWN = 0; 8 | // da dada dum 9 | STARTED = 1 [gah = 3]; 10 | RUNNING = 2; 11 | -------------------------------------------------------------------------------- /resources/erroneous/missing-bracket-msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Outer { 5 | message MiddleAA { 6 | message Inner { 7 | int64 ival = 1; 8 | bool booly = 2; 9 | } 10 | } 11 | message MiddleBB { 12 | message Inner { 13 | int32 ival = 1; 14 | bool booly = 2; 15 | message Deep { 16 | int32 xval = 1; 17 | enum Dowop { 18 | option allow_alias = true; 19 | UNKNOWN = 0; 20 | STARTING = 0; 21 | } 22 | enum Dowop2 { 23 | UNKNOWN2 = 0; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/erroneous/missing-msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | TaskDesc desc = 2; 7 | TaskDetails details = 3; 8 | } 9 | 10 | message TaskDesc { 11 | string desc = 1; 12 | } 13 | -------------------------------------------------------------------------------- /resources/erroneous/missing-package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | TaskDesc desc = 2; 7 | abcd.TaskDetails details = 3; 8 | } 9 | 10 | message TaskDesc { 11 | string desc = 1; 12 | } 13 | -------------------------------------------------------------------------------- /resources/erroneous/msg-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | service TaskMaster { 5 | message Task { 6 | string id = 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/erroneous/no-syntax.proto: -------------------------------------------------------------------------------- 1 | package missing; 2 | 3 | // EnumAllowingAlias docs for testing... 4 | enum EnumAllowingAlias { 5 | option rah = true; 6 | UNKNOWN = 0; 7 | // da dada dum 8 | STARTED = 1 [gah = 3]; 9 | } 10 | -------------------------------------------------------------------------------- /resources/erroneous/oneof-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | service TaskMaster { 5 | oneof State { 6 | int32 errcode = 128; 7 | string errmsg = 129; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/erroneous/optional-in-proto3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message TaskListOptions { 5 | string status = 1; 6 | optional string for = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/package-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Task { 4 | package missing; 5 | string id = 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/required-in-proto3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message TaskListOptions { 5 | string status = 1; 6 | required string for = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/rpc-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Empty {} 5 | 6 | message TaskListOptions { 7 | string status = 1; 8 | string for = 2; 9 | rpc Add (Empty) returns (Empty) {} 10 | } 11 | -------------------------------------------------------------------------------- /resources/erroneous/syntax-in-wrong-context.proto: -------------------------------------------------------------------------------- 1 | package missing; 2 | 3 | message Task { 4 | syntax = "proto3"; 5 | string id = 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/unused-import.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | import "dummy.proto"; 5 | 6 | message Task { 7 | string id = 1; 8 | TaskDesc desc = 2; 9 | } 10 | 11 | message TaskDesc { 12 | string desc = 1; 13 | } 14 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-enum-constant-tag.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | UNKNOWN = string; 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-extend.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | } 7 | 8 | extend Task 9 | int32 bar = 126; 10 | } 11 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-field.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package missing; 3 | 4 | message Task { 5 | string id != 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-import.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | import "duh/abcd.proto"; 5 | 6 | message Task { 7 | string id = 1; 8 | TaskDesc desc = 2; 9 | abcd.TaskDetails details = 3; 10 | } 11 | 12 | message TaskDesc { 13 | string desc = 1; 14 | } 15 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-import2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | import private "duh/abcd.proto"; 5 | 6 | message Task { 7 | string id = 1; 8 | TaskDesc desc = 2; 9 | abcd.TaskDetails details = 3; 10 | } 11 | 12 | message TaskDesc { 13 | string desc = 1; 14 | } 15 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-import3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | import "duh/abcd.proto; 5 | 6 | message Task { 7 | string id = 1; 8 | TaskDesc desc = 2; 9 | abcd.TaskDetails details = 3; 10 | } 11 | 12 | message TaskDesc { 13 | string desc = 1; 14 | } 15 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-inline-option.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | UNKNOWN = 0; 6 | STARTED = 1 [gah > 0]; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-label-in-oneof-field.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | oneof fizzbuzz { 7 | option zzz = true; 8 | string fizz = 12; 9 | repeated int32 buzz = 13; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-map-declaration.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | required map taskmap = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-map-in-oneof.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | oneof fizzbuzz { 7 | option zzz = true; 8 | string fizz = 12; 9 | map buzz = 13; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-map-key.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | map taskmap = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-map-key2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | map taskmap = 2; 7 | } 8 | 9 | message Empty { 10 | } 11 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-map-labels.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | required map taskmap = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task 5 | string id = 1; 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-oneof.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | oneof fizzbuzz 7 | string fizz = 12; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-option.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | option rah != true; 6 | UNKNOWN = 0; 7 | STARTED = 1 [gah = 3]; 8 | } 9 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-option2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | enum EnumAllowingAlias { 5 | option rah = true 6 | UNKNOWN = 0; 7 | STARTED = 1; 8 | } 9 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-public-import.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | import public "duh/abcd.proto"; 5 | 6 | message Task { 7 | string id = 1; 8 | TaskDesc desc = 2; 9 | abcd.TaskDetails details = 3; 10 | } 11 | 12 | message TaskDesc { 13 | string desc = 1; 14 | } 15 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-rpc-datatype.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string id = 1; 6 | } 7 | 8 | service LogTask { 9 | rpc AddTask (Task) returns (TaskId) {} 10 | } 11 | 12 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing 3 | 4 | message Task { 5 | string id = 1; 6 | } 7 | 8 | service TaskMaster { 9 | rpc Add (Task) returns (TaskId); 10 | rpc Get (TaskId) gives (Task); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-rpc2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing 3 | 4 | message Task { 5 | string id = 1; 6 | } 7 | 8 | service TaskMaster { 9 | rpc Add (Task) returns (TaskId); 10 | rpc Get (TaskId) returns (Task) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package missing; 3 | 4 | message Task { 5 | string name = 1; 6 | } 7 | 8 | message TaskId { 9 | string id = 1; 10 | } 11 | 12 | service TaskMaster 13 | rpc Add (Task) returns (TaskId) {} 14 | } 15 | 16 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-syntax.proto: -------------------------------------------------------------------------------- 1 | syntax = "proton"; 2 | 3 | package missing; 4 | 5 | enum Empty { 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-syntax2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2" 2 | 3 | package missing; 4 | 5 | message Empty { 6 | } 7 | -------------------------------------------------------------------------------- /resources/erroneous/wrong-syntax3.proto: -------------------------------------------------------------------------------- 1 | syntax != "proto2"; 2 | 3 | package missing; 4 | 5 | message Empty { 6 | } 7 | -------------------------------------------------------------------------------- /resources/internal/README.txt: -------------------------------------------------------------------------------- 1 | Protoc is run on the files in this dir. The resultant error messages then give me 2 | a clue as to the deficiencies in pbparser. 3 | 4 | Command: protoc -I internal/ ./internal/proto2_test.proto --go_out=plugins=grpc:internal 5 | 6 | Run it from previous dir. 7 | -------------------------------------------------------------------------------- /resources/internal/ext/privatex.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package privatex; 3 | 4 | message Meh { 5 | string name = 1; 6 | string pincode = 2; 7 | } 8 | -------------------------------------------------------------------------------- /resources/internal/proto2_test.proto: -------------------------------------------------------------------------------- 1 | /* Task object blah blah adsd 2 | sfsf fafasf affaf fasfaf 3 | */ 4 | syntax = "proto2"; 5 | package logtask; 6 | 7 | //import public "new.proto"; 8 | //import "other.proto"; 9 | 10 | // LogTask is a service which handles operations on tasks defined via a custom DSL 11 | service LogTask { 12 | rpc AddTask (Task) returns (TaskId) {} 13 | rpc ListTasks(TaskListOptions) returns (TaskList) {} 14 | rpc UpdateTask(TaskUpdateOptions) returns (ReturnStatus) {} 15 | rpc DeleteTask(TaskId) returns (ReturnStatus) {} 16 | rpc RouteChat(TaskId) returns (stream ReturnStatus) {} 17 | } 18 | 19 | service AuditTask { 20 | rpc AuditTask (Task) returns (TaskId) {} 21 | } 22 | 23 | // Id of the Task... 24 | message TaskId { 25 | required string id = 1; 26 | } 27 | 28 | // Task object... 29 | message Task { 30 | required string name = 1; 31 | required string id = 2; 32 | optional string desc = 3; 33 | required string priority = 4; 34 | required string for = 5; 35 | required string on = 6; 36 | required string starting = 7; 37 | required string remind = 8; 38 | required string location = 9; 39 | repeated string tags = 10; 40 | repeated string comments = 11; 41 | oneof fizzbuzz { 42 | string fizz = 12; 43 | int32 buzz = 13; 44 | } 45 | } 46 | 47 | // List of tasks... 48 | message TaskList { 49 | repeated Task tasks = 1; 50 | } 51 | 52 | // Options to pass in a params for listing tasks... 53 | message TaskListOptions { 54 | required string status = 1; 55 | required string for = 2; 56 | extensions 500 to 600; 57 | } 58 | 59 | extend TaskListOptions { 60 | optional int32 bar = 510; 61 | } 62 | 63 | // Options to pass in for updating a task... 64 | message TaskUpdateOptions { 65 | required TaskId taskId = 1; 66 | required Task task = 2; 67 | reserved 13, 12, 9 to 11; 68 | } 69 | 70 | // Return status of delete and update task operations... 71 | message ReturnStatus { 72 | required bool success = 1; 73 | optional string message = 2; 74 | reserved "foo", "bar"; 75 | } 76 | 77 | message SearchResponse { 78 | message Result { 79 | required string url = 1; 80 | required string title = 2; 81 | repeated string snippets = 3; 82 | } 83 | repeated Result result = 1; 84 | map statusmap = 2; 85 | } 86 | 87 | //extend SearchResponse { 88 | // map statusmap = 310; 89 | //} 90 | 91 | // EnumAllowingAlias docs for testing... 92 | enum EnumAllowingAlias { 93 | UNKNOWN = 0; 94 | STARTED = 1; 95 | RUNNING = 2; 96 | } 97 | -------------------------------------------------------------------------------- /resources/internal/proto3_test.proto: -------------------------------------------------------------------------------- 1 | /* Task object blah blah adsd 2 | sfsf fafasf affaf fasfaf 3 | */ 4 | syntax = "proto3"; 5 | package logtask; 6 | 7 | //import public "new.proto"; 8 | //import "other.proto"; 9 | 10 | // LogTask is a service which handles operations on tasks defined via a custom DSL 11 | service LogTask { 12 | rpc AddTask (Task) returns (TaskId) {} 13 | rpc ListTasks(TaskListOptions) returns (TaskList) {} 14 | rpc UpdateTask(TaskUpdateOptions) returns (ReturnStatus) {} 15 | rpc DeleteTask(TaskId) returns (ReturnStatus) {} 16 | rpc RouteChat(TaskId) returns (stream ReturnStatus) {} 17 | } 18 | 19 | service AuditTask { 20 | rpc AuditTask (Task) returns (TaskId) {} 21 | } 22 | 23 | // Id of the Task... 24 | message TaskId { 25 | string id = 1; 26 | } 27 | 28 | // Task object... 29 | message Task { 30 | string name = 1; 31 | string id = 2; 32 | string desc = 3; 33 | string priority = 4; 34 | string for = 5; 35 | string on = 6; 36 | string starting = 7; 37 | string remind = 8; 38 | string location = 9; 39 | repeated string tags = 10; 40 | repeated string comments = 11; 41 | oneof fizzbuzz { 42 | string fizz = 12; 43 | int32 buzz = 13; 44 | } 45 | } 46 | 47 | // List of tasks... 48 | message TaskList { 49 | repeated Task tasks = 1; 50 | } 51 | 52 | // Options to pass in a params for listing tasks... 53 | message TaskListOptions { 54 | string status = 1; 55 | string for = 2; 56 | //extensions 500 to 600; 57 | } 58 | 59 | //extend TaskListOptions { 60 | // int32 bar = 510; 61 | //} 62 | 63 | // Options to pass in for updating a task... 64 | message TaskUpdateOptions { 65 | TaskId taskId = 1; 66 | Task task = 2; 67 | reserved 13, 12, 9 to 11; 68 | } 69 | 70 | // Return status of delete and update task operations... 71 | message ReturnStatus { 72 | bool success = 1; 73 | string message = 2; 74 | reserved "foo", "bar"; 75 | } 76 | 77 | // EnumAllowingAlias docs for testing... 78 | enum EnumAllowingAlias { 79 | UNKNOWN = 0; 80 | STARTED = 1; 81 | RUNNING = 2; 82 | } 83 | -------------------------------------------------------------------------------- /resources/internal/publicx.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package publicx; 3 | 4 | message Duh { 5 | string id = 1; 6 | } 7 | 8 | // StatusEnum docs for testing StatusEnum... 9 | enum StatusEnum { 10 | UP = 0; 11 | DOWN = 1; 12 | } 13 | 14 | message SearchRequest { 15 | message Request { 16 | string url = 1; 17 | string title = 2; 18 | repeated string snippets = 3; 19 | } 20 | repeated Request request = 1; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /resources/service.proto: -------------------------------------------------------------------------------- 1 | /* Task object blah blah adsd 2 | sfsf fafasf affaf fasfaf 3 | */ 4 | syntax = "proto2"; 5 | package logtask; 6 | 7 | option java_package = "com.google.protobuf"; 8 | 9 | import public "internal/publicx.proto"; 10 | import "internal/ext/privatex.proto"; 11 | 12 | // LogTask is a service which handles operations on tasks defined via a custom DSL 13 | service LogTask { 14 | // foosh doc 15 | option foosh = true; 16 | // AddTask doc 17 | rpc AddTask (Task) returns (TaskId) {} 18 | rpc ListTasks(TaskListOptions) returns (TaskList) {} 19 | rpc UpdateTask(TaskUpdateOptions) returns (ReturnStatus) {} 20 | rpc DeleteTask(TaskId) returns (ReturnStatus) { 21 | option crap = true; 22 | } 23 | rpc RouteChat(stream publicx.Duh) returns (stream privatex.Meh) {} 24 | rpc RouteCall(stream SearchResponse.Result) returns (stream publicx.SearchRequest.Request) {} 25 | rpc ServeNestedObject(TaskId) returns (stream Outer.MiddleAA.Inner){} 26 | } 27 | 28 | // Id of the Task... 29 | message TaskId { 30 | option message_set_wire_format = true; 31 | string id = 1; 32 | enum Corpus { 33 | UNIVERSAL = 0; 34 | WEB = 1; 35 | IMAGES = 2; 36 | LOCAL = 3; 37 | NEWS = 4; 38 | PRODUCTS = 5; 39 | VIDEO = 6; 40 | } 41 | optional Corpus corpus = 3 [default = UNIVERSAL]; 42 | } 43 | 44 | // Task object... 45 | message Task { 46 | string name = 1; 47 | string id = 2; 48 | string desc = 3; 49 | string priority = 4 [deprecated=true,default="p1"]; 50 | string for = 5; 51 | string on = 6; 52 | string starting = 7; 53 | string remind = 8 [deprecated=true]; 54 | string location = 9 [default="mars"]; 55 | repeated string tags = 10; 56 | repeated string comments = 11; 57 | // fizzed 58 | oneof fizzbuzz { 59 | option zzz = true; 60 | string fizz = 12; 61 | int32 buzz = 13; 62 | } 63 | } 64 | 65 | extend Task { 66 | int32 bar = 126; 67 | } 68 | 69 | // List of tasks... 70 | message TaskList { 71 | repeated Task tasks = 1; 72 | extend Task { 73 | optional int32 barone = 127; 74 | } 75 | } 76 | 77 | // Options to pass in a params for listing tasks... 78 | message TaskListOptions { 79 | string status = 1; 80 | string for = 2; 81 | extensions 1000 to max; 82 | } 83 | 84 | // Options to pass in for updating a task... 85 | message TaskUpdateOptions { 86 | TaskId taskId = 1; 87 | Task task = 2; 88 | reserved 10, 12, 9 to 11; 89 | } 90 | 91 | // Return status of delete and update task operations... 92 | message ReturnStatus { 93 | bool success = 1; 94 | string message = 2; 95 | publicx.StatusEnum status = 3; 96 | //Drama drama = 4; 97 | reserved "foo", "bar"; 98 | } 99 | 100 | message SearchResponse { 101 | message Result { 102 | required string url = 1; 103 | string title = 2; 104 | repeated string snippets = 3; 105 | } 106 | repeated Result result = 1; 107 | map statusmap = 2; 108 | enum EnumNotAllowingAlias { 109 | UNKNOWN = 0; 110 | } 111 | } 112 | 113 | // EnumAllowingAlias docs for testing... 114 | enum EnumAllowingAlias { 115 | UNKNOWN = 0; //abcd 116 | STARTED = 1; 117 | RUNNING = 2; 118 | } 119 | 120 | message Outer { 121 | message MiddleAA { 122 | message Inner { 123 | int64 ival = 1; 124 | bool booly = 2; 125 | } 126 | } 127 | message MiddleBB { 128 | message Inner { 129 | int32 ival = 1; 130 | bool booly = 2; 131 | message Deep { 132 | int32 xval = 1; 133 | enum Dowop { 134 | option allow_alias = true; 135 | UNKNOWN = 0; 136 | STARTING = 0; 137 | } 138 | enum Dowop2 { 139 | UNKNOWN2 = 0; 140 | } 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /verifier.go: -------------------------------------------------------------------------------- 1 | package pbparser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type protoFileOracle struct { 10 | pf *ProtoFile 11 | msgmap map[string]bool 12 | enummap map[string]bool 13 | } 14 | 15 | func verify(pf *ProtoFile, p ImportModuleProvider) error { 16 | // validate syntax 17 | if err := validateSyntax(pf); err != nil { 18 | return err 19 | } 20 | 21 | if (len(pf.Dependencies) > 0 || len(pf.PublicDependencies) > 0) && p == nil { 22 | return errors.New("ImportModuleProvider is required to validate imports") 23 | } 24 | 25 | // make a map of package to its oracle... 26 | m := make(map[string]protoFileOracle) 27 | 28 | // parse the dependencies... 29 | if err := parseDependencies(p, pf.Dependencies, m); err != nil { 30 | return err 31 | } 32 | // parse the public dependencies... 33 | if err := parseDependencies(p, pf.PublicDependencies, m); err != nil { 34 | return err 35 | } 36 | 37 | // make oracle for main package and add to map... 38 | orcl := protoFileOracle{pf: pf} 39 | orcl.msgmap, orcl.enummap = makeQNameLookup(pf) 40 | if _, found := m[pf.PackageName]; found { 41 | for k, v := range orcl.msgmap { 42 | m[pf.PackageName].msgmap[k] = v 43 | } 44 | for k, v := range orcl.enummap { 45 | m[pf.PackageName].enummap[k] = v 46 | } 47 | 48 | // update the main model as well in case it is defined across multiple files 49 | merge(pf, m[pf.PackageName].pf) 50 | } else { 51 | m[pf.PackageName] = orcl 52 | } 53 | 54 | // collate the dependency package names... 55 | packageNames := getDependencyPackageNames(pf.PackageName, m) 56 | 57 | // check if imported packages are in use 58 | if err := areImportedPackagesUsed(pf, packageNames); err != nil { 59 | return err 60 | } 61 | 62 | // validate if the NamedDataType fields of messages (deep ones as well) are all defined in the model; 63 | // either the main model or in dependencies 64 | fields := []fd{} 65 | findFieldsToValidate(pf.Messages, &fields) 66 | for _, f := range fields { 67 | if err := validateFieldDataTypes(pf.PackageName, f, pf.Messages, pf.Enums, m, packageNames); err != nil { 68 | return err 69 | } 70 | } 71 | 72 | // validate if each rpc request/response type is defined in the model; 73 | // either the main model or in dependencies 74 | for _, s := range pf.Services { 75 | for _, rpc := range s.RPCs { 76 | if err := validateRPCDataType(pf.PackageName, s.Name, rpc.Name, rpc.RequestType, pf.Messages, m, packageNames); err != nil { 77 | return err 78 | } 79 | if err := validateRPCDataType(pf.PackageName, s.Name, rpc.Name, rpc.ResponseType, pf.Messages, m, packageNames); err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | 85 | // validate that message and enum names are unique in the package as well as at the nested msg level (howsoever deep) 86 | if err := validateUniqueMessageEnumNames("package "+pf.PackageName, pf.Enums, pf.Messages); err != nil { 87 | return err 88 | } 89 | 90 | // validate if enum constants are unique across enums in the package 91 | if err := validateEnumConstants("package "+pf.PackageName, pf.Enums); err != nil { 92 | return err 93 | } 94 | // validate if enum constants are unique across nested enums within nested messages (howsoever deep) 95 | for _, msg := range pf.Messages { 96 | if err := validateEnumConstantsInMessage(msg); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | // allow aliases in enums only if option allow_alias is specified 102 | if err := validateEnumConstantTagAliases(pf.Enums); err != nil { 103 | return err 104 | } 105 | // allow aliases in nested enums within nested messages (howsoever deep) only if option allow_alias is specified 106 | for _, msg := range pf.Messages { 107 | if err := validateEnumConstantTagAliasesInMessage(msg); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | // TODO: add more checks here if needed 113 | 114 | return nil 115 | } 116 | 117 | func merge(dest *ProtoFile, src *ProtoFile) { 118 | for _, d := range src.Dependencies { 119 | dest.Dependencies = append(dest.Dependencies, d) 120 | } 121 | for _, d := range src.PublicDependencies { 122 | dest.PublicDependencies = append(dest.PublicDependencies, d) 123 | } 124 | for _, d := range src.Options { 125 | dest.Options = append(dest.Options, d) 126 | } 127 | for _, d := range src.Messages { 128 | dest.Messages = append(dest.Messages, d) 129 | } 130 | for _, d := range src.Enums { 131 | dest.Enums = append(dest.Enums, d) 132 | } 133 | for _, d := range src.ExtendDeclarations { 134 | dest.ExtendDeclarations = append(dest.ExtendDeclarations, d) 135 | } 136 | } 137 | 138 | func areImportedPackagesUsed(pf *ProtoFile, packageNames []string) error { 139 | for _, pkg := range packageNames { 140 | var inuse bool 141 | // check if any request/response types are referring to this imported package... 142 | for _, service := range pf.Services { 143 | for _, rpc := range service.RPCs { 144 | if usesPackage(rpc.RequestType.Name(), pkg, packageNames) { 145 | inuse = true 146 | goto LABEL 147 | } 148 | if usesPackage(rpc.ResponseType.Name(), pkg, packageNames) { 149 | inuse = true 150 | goto LABEL 151 | } 152 | } 153 | } 154 | // check if any fields in messages (nested or not) are referring to this imported package... 155 | if checkImportedPackageUsage(pf.Messages, pkg, packageNames) { 156 | inuse = true 157 | } 158 | LABEL: 159 | if !inuse { 160 | return errors.New("Imported package: " + pkg + " but not used") 161 | } 162 | } 163 | return nil 164 | } 165 | 166 | func checkImportedPackageUsage(msgs []MessageElement, pkg string, packageNames []string) bool { 167 | for _, msg := range msgs { 168 | for _, f := range msg.Fields { 169 | if f.Type.Category() == NamedDataTypeCategory && usesPackage(f.Type.Name(), pkg, packageNames) { 170 | return true 171 | } 172 | } 173 | if len(msg.Messages) > 0 { 174 | if checkImportedPackageUsage(msg.Messages, pkg, packageNames) { 175 | return true 176 | } 177 | } 178 | } 179 | return false 180 | } 181 | 182 | func usesPackage(s string, pkg string, packageNames []string) bool { 183 | if strings.ContainsRune(s, '.') { 184 | inSamePkg, pkgName := isDatatypeInSamePackage(s, packageNames) 185 | if !inSamePkg && pkg == pkgName { 186 | return true 187 | } 188 | } 189 | return false 190 | } 191 | 192 | func validateUniqueMessageEnumNames(ctxName string, enums []EnumElement, msgs []MessageElement) error { 193 | m := make(map[string]bool) 194 | for _, en := range enums { 195 | if m[en.Name] { 196 | return errors.New("Duplicate name " + en.Name + " in " + ctxName) 197 | } 198 | m[en.Name] = true 199 | } 200 | for _, msg := range msgs { 201 | if m[msg.Name] { 202 | return errors.New("Duplicate name " + msg.Name + " in " + ctxName) 203 | } 204 | m[msg.Name] = true 205 | } 206 | for _, msg := range msgs { 207 | if err := validateUniqueMessageEnumNames("message "+msg.Name, msg.Enums, msg.Messages); err != nil { 208 | return err 209 | } 210 | } 211 | return nil 212 | } 213 | 214 | func validateEnumConstantTagAliases(enums []EnumElement) error { 215 | for _, en := range enums { 216 | m := make(map[int]bool) 217 | for _, enc := range en.EnumConstants { 218 | if m[enc.Tag] { 219 | if !isAllowAlias(&en) { 220 | return errors.New(enc.Name + " is reusing an enum value. If this is intended, set 'option allow_alias = true;' in the enum") 221 | } 222 | } 223 | m[enc.Tag] = true 224 | } 225 | } 226 | return nil 227 | } 228 | 229 | func validateEnumConstantTagAliasesInMessage(msg MessageElement) error { 230 | if err := validateEnumConstantTagAliases(msg.Enums); err != nil { 231 | return err 232 | } 233 | for _, nestedmsg := range msg.Messages { 234 | if err := validateEnumConstantTagAliasesInMessage(nestedmsg); err != nil { 235 | return err 236 | } 237 | } 238 | return nil 239 | } 240 | 241 | func isAllowAlias(en *EnumElement) bool { 242 | for _, op := range en.Options { 243 | if op.Name == "allow_alias" && op.Value == "true" { 244 | return true 245 | } 246 | } 247 | return false 248 | } 249 | 250 | func validateEnumConstants(ctxName string, enums []EnumElement) error { 251 | m := make(map[string]bool) 252 | for _, en := range enums { 253 | for _, enc := range en.EnumConstants { 254 | if m[enc.Name] { 255 | return errors.New("Enum constant " + enc.Name + " is already defined in " + ctxName) 256 | } 257 | m[enc.Name] = true 258 | } 259 | } 260 | return nil 261 | } 262 | 263 | func validateEnumConstantsInMessage(msg MessageElement) error { 264 | if err := validateEnumConstants("message "+msg.Name, msg.Enums); err != nil { 265 | return err 266 | } 267 | for _, nestedmsg := range msg.Messages { 268 | if err := validateEnumConstantsInMessage(nestedmsg); err != nil { 269 | return err 270 | } 271 | } 272 | return nil 273 | } 274 | 275 | func validateSyntax(pf *ProtoFile) error { 276 | if pf.Syntax == "" { 277 | return errors.New("No syntax specified in the proto file") 278 | } 279 | return nil 280 | } 281 | 282 | func getDependencyPackageNames(mainPkgName string, m map[string]protoFileOracle) []string { 283 | var keys []string 284 | for k := range m { 285 | if k == mainPkgName { 286 | continue 287 | } 288 | keys = append(keys, k) 289 | } 290 | return keys 291 | } 292 | 293 | func makeQNameLookup(dpf *ProtoFile) (map[string]bool, map[string]bool) { 294 | msgmap := make(map[string]bool) 295 | enummap := make(map[string]bool) 296 | for _, msg := range dpf.Messages { 297 | msgmap[msg.QualifiedName] = true 298 | gatherNestedQNames(msg, msgmap, enummap) 299 | } 300 | for _, en := range dpf.Enums { 301 | enummap[en.QualifiedName] = true 302 | } 303 | return msgmap, enummap 304 | } 305 | 306 | func gatherNestedQNames(parentmsg MessageElement, msgmap map[string]bool, enummap map[string]bool) { 307 | for _, nestedmsg := range parentmsg.Messages { 308 | msgmap[nestedmsg.QualifiedName] = true 309 | gatherNestedQNames(nestedmsg, msgmap, enummap) 310 | } 311 | for _, en := range parentmsg.Enums { 312 | enummap[en.QualifiedName] = true 313 | } 314 | } 315 | 316 | type fd struct { 317 | name string 318 | category string 319 | msg MessageElement 320 | } 321 | 322 | func findFieldsToValidate(msgs []MessageElement, fields *[]fd) { 323 | for _, msg := range msgs { 324 | for _, f := range msg.Fields { 325 | if f.Type.Category() == NamedDataTypeCategory { 326 | *fields = append(*fields, fd{name: f.Name, category: f.Type.Name(), msg: msg}) 327 | } 328 | } 329 | if len(msg.Messages) > 0 { 330 | findFieldsToValidate(msg.Messages, fields) 331 | } 332 | } 333 | } 334 | 335 | func validateFieldDataTypes(mainpkg string, f fd, msgs []MessageElement, enums []EnumElement, m map[string]protoFileOracle, packageNames []string) error { 336 | var found bool 337 | if strings.ContainsRune(f.category, '.') { 338 | inSamePkg, pkgName := isDatatypeInSamePackage(f.category, packageNames) 339 | if inSamePkg { 340 | orcl := m[mainpkg] 341 | 342 | var msgMatchTerm, enumMatchTerm string 343 | if !strings.HasPrefix(f.category, mainpkg+".") { 344 | msgMatchTerm = mainpkg + "." + f.category 345 | enumMatchTerm = mainpkg + "." + f.category 346 | } else { 347 | msgMatchTerm = f.category 348 | enumMatchTerm = f.category 349 | } 350 | 351 | // Check against normal and nested messages & enums in same package 352 | found = orcl.msgmap[msgMatchTerm] 353 | if !found { 354 | found = orcl.enummap[enumMatchTerm] 355 | } 356 | } else { 357 | orcl := m[pkgName] 358 | // Check against normal and nested messages & enums in dependency package 359 | found = orcl.msgmap[f.category] 360 | if !found { 361 | found = orcl.enummap[f.category] 362 | } 363 | } 364 | } else { 365 | // Check any nested messages and nested enums in the same message which has the field 366 | found = checkMsgOrEnumName(f.category, f.msg.Messages, f.msg.Enums) 367 | // If not a nested message or enum, then just check first class messages & enums in the package 368 | if !found { 369 | found = checkMsgOrEnumName(f.category, msgs, enums) 370 | } 371 | } 372 | if !found { 373 | msg := fmt.Sprintf("Datatype: '%v' referenced in field: '%v' is not defined", f.category, f.name) 374 | return errors.New(msg) 375 | } 376 | return nil 377 | } 378 | 379 | func validateRPCDataType(mainpkg string, service string, rpc string, datatype NamedDataType, msgs []MessageElement, m map[string]protoFileOracle, packageNames []string) error { 380 | var found bool 381 | if strings.ContainsRune(datatype.Name(), '.') { 382 | inSamePkg, pkgName := isDatatypeInSamePackage(datatype.Name(), packageNames) 383 | if inSamePkg { 384 | // Check against normal as well as nested types in same package 385 | orcl := m[mainpkg] 386 | found = orcl.msgmap[mainpkg+"."+datatype.Name()] 387 | } else { 388 | orcl := m[pkgName] 389 | // Check against normal and nested messages & enums in dependency package 390 | found = orcl.msgmap[datatype.Name()] 391 | } 392 | } else { 393 | found = checkMsgName(datatype.Name(), msgs) 394 | } 395 | if !found { 396 | msg := fmt.Sprintf("Datatype: '%v' referenced in RPC: '%v' of Service: '%v' is not defined OR is not a message type", datatype.Name(), rpc, service) 397 | return errors.New(msg) 398 | } 399 | return nil 400 | } 401 | 402 | func isDatatypeInSamePackage(datatypeName string, packageNames []string) (bool, string) { 403 | for _, pkg := range packageNames { 404 | if strings.HasPrefix(datatypeName, pkg+".") { 405 | return false, pkg 406 | } 407 | } 408 | return true, "" 409 | } 410 | 411 | func checkMsgOrEnumName(s string, msgs []MessageElement, enums []EnumElement) bool { 412 | if checkMsgName(s, msgs) { 413 | return true 414 | } 415 | return checkEnumName(s, enums) 416 | } 417 | 418 | func checkMsgName(m string, msgs []MessageElement) bool { 419 | for _, msg := range msgs { 420 | if msg.Name == m { 421 | return true 422 | } 423 | } 424 | return false 425 | } 426 | 427 | func checkEnumName(s string, enums []EnumElement) bool { 428 | for _, en := range enums { 429 | if en.Name == s { 430 | return true 431 | } 432 | } 433 | return false 434 | } 435 | 436 | func parseDependencies(impr ImportModuleProvider, dependencies []string, m map[string]protoFileOracle) error { 437 | for _, d := range dependencies { 438 | r, err := impr.Provide(d) 439 | if err != nil { 440 | msg := fmt.Sprintf("ImportModuleReader is unable to provide content of dependency module %v. Reason:: %v", d, err.Error()) 441 | return errors.New(msg) 442 | } 443 | if r == nil { 444 | msg := fmt.Sprintf("ImportModuleReader is unable to provide reader for dependency module %v", d) 445 | return errors.New(msg) 446 | } 447 | 448 | dpf := ProtoFile{} 449 | if err := parse(r, &dpf); err != nil { 450 | msg := fmt.Sprintf("Unable to parse dependency %v. Reason:: %v", d, err.Error()) 451 | return errors.New(msg) 452 | } 453 | 454 | // validate syntax 455 | if err := validateSyntax(&dpf); err != nil { 456 | return err 457 | } 458 | 459 | orcl := protoFileOracle{pf: &dpf} 460 | orcl.msgmap, orcl.enummap = makeQNameLookup(&dpf) 461 | 462 | if _, found := m[dpf.PackageName]; found { 463 | for k, v := range orcl.msgmap { 464 | m[dpf.PackageName].msgmap[k] = v 465 | } 466 | for k, v := range orcl.enummap { 467 | m[dpf.PackageName].enummap[k] = v 468 | } 469 | } else { 470 | m[dpf.PackageName] = orcl 471 | } 472 | } 473 | return nil 474 | } 475 | --------------------------------------------------------------------------------