blob: 0e0f00cec8f5fa78fbf8809b2f0f88d5d9344af5 [file] [log] [blame]
Martin Cosynsf88ed6e2020-12-02 10:30:10 +01001# Copyright 2020 ADTRAN, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13import os
14import re
15import glob
16import json
17import argparse
18
19from . import protobuf_parse as parser
20
21__version__ = '1.0'
22
23USAGE = """ProtoBuf -- Parser of Protocol Buffer to create the input JSON file for the library
24
25Usage: grpc_robot.protop [options] target_version
26
27ProtoBuf parser can be used to parse ProtoBuf files (*.proto) into a json formatted input file
28for the grpc_robot library to be used for keyword documentation.
29
30"""
31
32EPILOG = """
33Example
34=======
35# Executing `grpc_robot.protop` module using Python.
36$ grpc_robot.protop -i /home/user/Workspace/grpc/proto/dmi 0.9.1
37"""
38
39
40class ProtoBufParser(object):
41
42 def __init__(self, target, target_version, input_dir, output_dir=None):
43
44 super().__init__()
45
46 self.target = target
47 self.target_version = target_version.replace('.', '_')
48 self.input_dir = input_dir
49 self.output_dir = output_dir
50
51 @staticmethod
52 def read_enum(enum, protobuf_dict, module):
53 enum_dict = {'name': enum.name, 'type': 'enum', 'module': module, 'values': {ef.value: ef.name for ef in enum.body}}
54 protobuf_dict['data_types'].append(enum_dict)
55
56 def read_message(self, message, protobuf_dict, module):
57 message_dict = {'name': message.name, 'type': 'message', 'module': module, 'fields': []}
58
59 for f in message.body:
60 if isinstance(f, parser.Enum):
61 self.read_enum(f, protobuf_dict, module)
62 continue
63
64 elif isinstance(f, parser.Message):
65 self.read_message(f, protobuf_dict, module)
66 continue
67
68 field_dict = {'name': f.name, 'is_choice': isinstance(f, parser.OneOf)}
69
70 if isinstance(f, parser.Field):
71 field_dict['repeated'] = f.repeated
72
73 try:
74 field_dict['type'] = f.type._value_
75 field_dict['lookup'] = False
76 except AttributeError:
77 field_dict['type'] = f.type
78 field_dict['lookup'] = True
79
80 elif isinstance(f, parser.OneOf):
81 field_dict['cases'] = []
82 for c in f.fields:
83 case_dict = {'name': c.name}
84 try:
85 case_dict['type'] = c.type._value_
86 case_dict['lookup'] = False
87 except AttributeError:
88 case_dict['type'] = c.type
89 case_dict['lookup'] = True
90 field_dict['cases'].append(case_dict)
91
92 message_dict['fields'].append(field_dict)
93
94 protobuf_dict['data_types'].append(message_dict)
95
96 def parse_files(self):
97
98 protobuf_dict = {
99 'modules': [],
100 'data_types': [],
101 'services': []
102 }
103
104 for file_name in glob.glob(os.path.join(self.input_dir, '*.proto')):
105
106 module = os.path.splitext(os.path.basename(file_name))[0]
107 module_dict = {'name': module, 'imports': []}
108
109 # the protobuf parser can not handle comments "// ...", so remove them first from the file
110 file_content = re.sub(r'\/\/.*', '', open(file_name).read())
111 parsed = parser.proto.parse(file_content)
112
113 for p in parsed.statements:
114
115 if isinstance(p, parser.Import):
116 module_dict['imports'].append(os.path.splitext(os.path.basename(p.identifier))[0])
117
118 elif isinstance(p, parser.Enum):
119 self.read_enum(p, protobuf_dict, module)
120
121 elif isinstance(p, parser.Message):
122 self.read_message(p, protobuf_dict, module)
123
124 elif isinstance(p, parser.Service):
125 service_dict = {'name': p.name, 'module': module, 'rpcs': []}
126
127 for field in p.body:
128
129 if isinstance(field, parser.Enum):
130 self.read_enum(field, protobuf_dict, module)
131
132 elif isinstance(field, parser.Message):
133 self.read_message(field, protobuf_dict, module)
134
135 elif isinstance(field, parser.Rpc):
136 rpc_dict = {'name': field.name, 'request': {}, 'response': {}}
137
138 for attr in ['request', 'response']:
139 try:
140 rpc_dict[attr]['is_stream'] = field.__getattribute__('%s_stream' % attr)
141
142 try:
143 rpc_dict[attr]['type'] = field.__getattribute__('%s_message_type' % attr)._value_
144 rpc_dict[attr]['lookup'] = False
145 except AttributeError:
146 rpc_dict[attr]['type'] = field.__getattribute__('%s_message_type' % attr)
147 rpc_dict[attr]['lookup'] = not rpc_dict[attr]['type'].lower().startswith('google.protobuf.')
148
149 except AttributeError:
150 rpc_dict[attr] = None
151
152 service_dict['rpcs'].append(rpc_dict)
153
154 protobuf_dict['services'].append(service_dict)
155
156 protobuf_dict['modules'].append(module_dict)
157
158 if self.output_dir is not None:
159 json_file_name = os.path.join(self.output_dir, self.target, '%s_%s' % (self.target, self.target_version), '%s.json' % self.target)
160 json.dump(protobuf_dict, open(json_file_name, 'w'))
161
162 return protobuf_dict
163
164
165base_dir = os.path.dirname(os.path.realpath(__file__))
166output_base_dir = os.path.join(os.path.split(base_dir)[:-1][0], 'services')
167
168
169def main():
170 # create commandline parser
171 arg_parse = argparse.ArgumentParser(description=USAGE, epilog=EPILOG, formatter_class=argparse.RawTextHelpFormatter)
172
173 # add parser options
174 arg_parse.add_argument('target', choices=['dmi'], default='dmi',
175 help="Target type of which the ProtocolBuffer files shall be converted to the JSON file.")
176 arg_parse.add_argument('target_version', help="Version number of the ProtocolBuffer files.")
177
178 arg_parse.add_argument('-i', '--inputdir', default=os.getcwd(), help="Path to the location of the ProtocolBuffer files.")
179 arg_parse.add_argument('-o', '--outputdir', default=os.getcwd(), help="Path to the location JSON file to be stored.")
180
181 arg_parse.add_argument('-v', '--version', action='version', version=__version__)
182 arg_parse.set_defaults(feature=False)
183
184 # parse commandline
185 args = arg_parse.parse_args()
186
187 ProtoBufParser(args.target, args.target_version, args.inputdir or os.getcwd(), args.outputdir or output_base_dir).parse_files()
188
189
190if __name__ == '__main__':
191 main()