blob: 88ce1a7545d961d1e28d8d009c07d818357ae131 [file] [log] [blame]
Khen Nursimulud7688092016-11-17 00:08:57 -05001#!/usr/bin/env python
2#
3# Copyright 2016 the original author or authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""protoc plugin to convert a protobuf schema to a yang schema
19
20 - basic support for message, fields. enumeration, service, method
21
22 - yang semantic rules needs to be implemented
23
24 - to run this plugin :
25
26 $ python -m grpc.tools.protoc -I.
27 --plugin=protoc-gen-custom=./proto2yang.py --custom_out=. <proto file>.proto
28
29 - the above will produce a <proto file>.yang file formatted for yang
30
31 - two examples of proto that can be used in the same directory are
32 yang.proto and addressbook.proto
33
34"""
35
36import sys
37
38from jinja2 import Template
39from google.protobuf.compiler import plugin_pb2 as plugin
40from descriptor_parser import DescriptorParser
41
42from google.protobuf.descriptor import FieldDescriptor
43
44template_yang = Template("""
45module {{ module.name }} {
46
47 namespace "https://gerrit.opencord.org/voltha/{{ module.package }}";
48 yang-version 1.1;
49
50 prefix "voltha";
51
52 revision 2016-11-15 {{ module.revision }} {
53 {% if module.description %}
54 /* {{ message.description }} */
55 {% else %}
56 description "Initial revision.";
57 {% endif %}
58 }
Khen Nursimulubd24f552016-11-18 11:31:15 -050059
60 {% for enum in module.enums %}
61 {% if enum.description %}
62 /* {{ enum.description }} */
63 {% endif %}
64 typedef {{ enum.name }} {
65 type enumeration {
66 {% for v in enum.value %}
67 {% if v.description %}
68 enum {{ v.name }} {
69 description "{{ v.description }}";
70 }
71 {% else %}
72 enum {{ v.name }} ;
73 {% endif %}
74 {% endfor %}
75 }
76 }
77 {% endfor %}
78
Khen Nursimulud7688092016-11-17 00:08:57 -050079 {% for message in module.messages recursive %}
Khen Nursimulubd24f552016-11-18 11:31:15 -050080 {% if message.name in module.referenced_messages %}
Khen Nursimulud7688092016-11-17 00:08:57 -050081 grouping {{ message.name }} {
Khen Nursimulubd24f552016-11-18 11:31:15 -050082 {% else %}
83 container {{ message.name }} {
84 {% endif %}
Khen Nursimulud7688092016-11-17 00:08:57 -050085 {% if message.description %}
86 /* {{ message.description }} */
87 {% endif %}
Khen Nursimulud7688092016-11-17 00:08:57 -050088 {% for field in message.fields %}
89 {% if field.type_ref %}
90 {% if field.description %}
91 /* {{ field.description }} */
92 {% endif %}
Khen Nursimulubd24f552016-11-18 11:31:15 -050093 {% for dict_item in module.referred_messages_with_keys %}
94 {% if dict_item.name == field.type %}
95 list {{ field.name }} {
96 key "{{ dict_item.key }}";
97 {% if not field.repeated %}
98 max-elements 1;
99 {% endif %}
100 uses {{ field.type }};
101 }
102 {% endif %}
103 {% endfor %}
104 {% elif field.repeated %}
105 list {{ field.name }} {
106 key "{{ field.name }}";
107 leaf {{ field.name }} {
108 {% if field.type == "decimal64" %}
109 type {{ field.type }} {
110 fraction-digits 5;
111 }
112 {% else %}
113 type {{ field.type }} ;
114 {% endif %}
115 {% if field.description %}
116 description
117 "{{ field.description }}" ;
118 {% endif %}
119 }
120 }
Khen Nursimulud7688092016-11-17 00:08:57 -0500121 {% else %}
122 leaf {{ field.name }} {
123 {% if field.type == "decimal64" %}
124 type {{ field.type }} {
125 fraction-digits 5;
126 }
127 {% else %}
128 type {{ field.type }} ;
129 {% endif %}
130 {% if field.description %}
131 description
132 "{{ field.description }}" ;
133 {% endif %}
134 }
135 {% endif %}
Khen Nursimulubd24f552016-11-18 11:31:15 -0500136
Khen Nursimulud7688092016-11-17 00:08:57 -0500137 {% endfor %}
138 {% for enum_type in message.enums %}
139 {% if enum_type.description %}
Khen Nursimulubd24f552016-11-18 11:31:15 -0500140 /* {{ enum_type.description }} */
Khen Nursimulud7688092016-11-17 00:08:57 -0500141 {% endif %}
142 typedef {{ enum_type.name }} {
143 type enumeration {
144 {% for v in enum_type.value %}
145 {% if v.description %}
146 enum {{ v.name }} {
147 description "{{ v.description }}";
148 }
149 {% else %}
150 enum {{ v.name }} ;
151 {% endif %}
152 {% endfor %}
153 }
154 }
Khen Nursimulubd24f552016-11-18 11:31:15 -0500155
Khen Nursimulud7688092016-11-17 00:08:57 -0500156 {% endfor %}
157 {% if message.messages %}
158 {{ loop (message.messages)|indent(4, false) }}
159 {% endif %}
160 }
Khen Nursimulud7688092016-11-17 00:08:57 -0500161
Khen Nursimulubd24f552016-11-18 11:31:15 -0500162 {% endfor %}
Khen Nursimulud7688092016-11-17 00:08:57 -0500163 {% for service in module.services %}
164 {% if service.description %}
165 /* {{ service.description }}" */
166 {% endif %}
Khen Nursimulud7688092016-11-17 00:08:57 -0500167 {% for method in service.methods %}
168 {% if method.description %}
169 /* {{ method.description }} */
170 {% endif %}
171 rpc {{ service.service }}-{{ method.method }} {
172 {% if method.input %}
173 input {
174 {% if method.input_ref %}
175 uses {{ method.input }} ;
176 {% else %}
177 leaf {{ method.input }} {
178 type {{ method.input }} ;
179 }
180 {% endif %}
181 }
182 {% endif %}
183 {% if method.output %}
184 output {
185 {% if method.output_ref %}
186 uses {{ method.output }} ;
187 {% else %}
188 leaf {{ method.output }} {
189 type {{ method.output }} ;
190 }
191 {% endif %}
192 }
193 {% endif %}
194 }
Khen Nursimulubd24f552016-11-18 11:31:15 -0500195
Khen Nursimulud7688092016-11-17 00:08:57 -0500196 {% endfor %}
Khen Nursimulubd24f552016-11-18 11:31:15 -0500197
Khen Nursimulud7688092016-11-17 00:08:57 -0500198 {% endfor %}
199}
200""", trim_blocks=True, lstrip_blocks=True)
201
202
Khen Nursimulubd24f552016-11-18 11:31:15 -0500203def traverse_messages(message_types, prefix, referenced_messages):
Khen Nursimulud7688092016-11-17 00:08:57 -0500204 messages = []
205 for message_type in message_types:
206 assert message_type['_type'] == 'google.protobuf.DescriptorProto'
Khen Nursimulubd24f552016-11-18 11:31:15 -0500207
208 # full_name = prefix + '-' + message_type['name']
209 full_name = message_type['name']
210
Khen Nursimulud7688092016-11-17 00:08:57 -0500211 # parse the fields
Khen Nursimulubd24f552016-11-18 11:31:15 -0500212 fields = traverse_fields(message_type.get('field', []), full_name,
213 referenced_messages)
Khen Nursimulud7688092016-11-17 00:08:57 -0500214
215 # parse the enums
Khen Nursimulubd24f552016-11-18 11:31:15 -0500216 enums = traverse_enums(message_type.get('enum_type', []), full_name)
Khen Nursimulud7688092016-11-17 00:08:57 -0500217
218 # parse nested messages
219 nested = message_type.get('nested_type', [])
Khen Nursimulubd24f552016-11-18 11:31:15 -0500220 nested_messages = traverse_messages(nested, full_name,
221 referenced_messages)
Khen Nursimulud7688092016-11-17 00:08:57 -0500222 messages.append(
223 {
Khen Nursimulubd24f552016-11-18 11:31:15 -0500224 'name': full_name,
Khen Nursimulud7688092016-11-17 00:08:57 -0500225 'fields': fields,
226 'enums': enums,
227 # 'extensions': extensions,
228 'messages': nested_messages,
Khen Nursimulubd24f552016-11-18 11:31:15 -0500229 'description': remove_unsupported_characters(
230 message_type.get('_description', '')),
Khen Nursimulud7688092016-11-17 00:08:57 -0500231 # 'extension_ranges': extension_ranges,
232 # 'oneof': oneof
233 }
234 )
235 return messages
236
237
Khen Nursimulubd24f552016-11-18 11:31:15 -0500238def traverse_fields(fields_desc, prefix, referenced_messages):
Khen Nursimulud7688092016-11-17 00:08:57 -0500239 fields = []
240 for field in fields_desc:
241 assert field['_type'] == 'google.protobuf.FieldDescriptorProto'
Khen Nursimulubd24f552016-11-18 11:31:15 -0500242 yang_base_type = is_base_type(field['type'])
243 _type = get_yang_type(field)
244 if not yang_base_type:
245 referenced_messages.append(_type)
246
Khen Nursimulud7688092016-11-17 00:08:57 -0500247 fields.append(
248 {
Khen Nursimulubd24f552016-11-18 11:31:15 -0500249 # 'name': prefix + '-' + field.get('name', ''),
Khen Nursimulud7688092016-11-17 00:08:57 -0500250 'name': field.get('name', ''),
251 'label': field.get('label', ''),
Khen Nursimulubd24f552016-11-18 11:31:15 -0500252 'repeated': field['label'] == FieldDescriptor.LABEL_REPEATED,
Khen Nursimulud7688092016-11-17 00:08:57 -0500253 'number': field.get('number', ''),
254 'options': field.get('options', ''),
255 'type_name': field.get('type_name', ''),
Khen Nursimulubd24f552016-11-18 11:31:15 -0500256 'type': _type,
257 'type_ref': not yang_base_type,
Khen Nursimulud7688092016-11-17 00:08:57 -0500258 'description': remove_unsupported_characters(field.get(
259 '_description', ''))
260 }
261 )
262 return fields
263
264
Khen Nursimulubd24f552016-11-18 11:31:15 -0500265def traverse_enums(enums_desc, prefix):
Khen Nursimulud7688092016-11-17 00:08:57 -0500266 enums = []
267 for enum in enums_desc:
268 assert enum['_type'] == 'google.protobuf.EnumDescriptorProto'
Khen Nursimulubd24f552016-11-18 11:31:15 -0500269 # full_name = prefix + '-' + enum.get('name', '')
270 full_name = enum.get('name', '')
Khen Nursimulud7688092016-11-17 00:08:57 -0500271 enums.append(
272 {
Khen Nursimulubd24f552016-11-18 11:31:15 -0500273 'name': full_name,
Khen Nursimulud7688092016-11-17 00:08:57 -0500274 'value': enum.get('value', ''),
Khen Nursimulubd24f552016-11-18 11:31:15 -0500275 'description': remove_unsupported_characters(enum.get(
276 '_description', ''))
Khen Nursimulud7688092016-11-17 00:08:57 -0500277 }
278 )
279 return enums
280
281
Khen Nursimulubd24f552016-11-18 11:31:15 -0500282def traverse_services(service_desc, referenced_messages):
Khen Nursimulud7688092016-11-17 00:08:57 -0500283 services = []
284 for service in service_desc:
285 methods = []
286 for method in service.get('method', []):
287 assert method['_type'] == 'google.protobuf.MethodDescriptorProto'
Khen Nursimulubd24f552016-11-18 11:31:15 -0500288
Khen Nursimulud7688092016-11-17 00:08:57 -0500289 input_name = method.get('input_type')
290 input_ref = False
291 if not is_base_type(input_name):
Khen Nursimulubd24f552016-11-18 11:31:15 -0500292 input_name = remove_first_character_if_match(input_name, '.')
293 # input_name = input_name.replace(".", "-")
Khen Nursimulud7688092016-11-17 00:08:57 -0500294 input_name = input_name.split('.')[-1]
Khen Nursimulubd24f552016-11-18 11:31:15 -0500295 referenced_messages.append(input_name)
Khen Nursimulud7688092016-11-17 00:08:57 -0500296 input_ref = True
Khen Nursimulubd24f552016-11-18 11:31:15 -0500297
Khen Nursimulud7688092016-11-17 00:08:57 -0500298 output_name = method.get('output_type')
299 output_ref = False
300 if not is_base_type(output_name):
Khen Nursimulubd24f552016-11-18 11:31:15 -0500301 output_name = remove_first_character_if_match(output_name, '.')
302 # output_name = output_name.replace(".", "-")
Khen Nursimulud7688092016-11-17 00:08:57 -0500303 output_name = output_name.split('.')[-1]
Khen Nursimulubd24f552016-11-18 11:31:15 -0500304 referenced_messages.append(output_name)
Khen Nursimulud7688092016-11-17 00:08:57 -0500305 output_ref = True
Khen Nursimulubd24f552016-11-18 11:31:15 -0500306
Khen Nursimulud7688092016-11-17 00:08:57 -0500307 methods.append(
308 {
309 'method': method.get('name', ''),
310 'input': input_name,
311 'input_ref': input_ref,
312 'output': output_name,
313 'output_ref': output_ref,
Khen Nursimulubd24f552016-11-18 11:31:15 -0500314 'description': remove_unsupported_characters(method.get(
315 '_description', '')),
Khen Nursimulud7688092016-11-17 00:08:57 -0500316 'server_streaming': method.get('server_streaming',
317 False) == True
318 }
319 )
320 services.append(
321 {
322 'service': service.get('name', ''),
323 'methods': methods,
Khen Nursimulubd24f552016-11-18 11:31:15 -0500324 'description': remove_unsupported_characters(service.get(
325 '_description', '')),
Khen Nursimulud7688092016-11-17 00:08:57 -0500326 }
327 )
328 return services
329
330
Khen Nursimulubd24f552016-11-18 11:31:15 -0500331def rchop(thestring, ending):
Khen Nursimulud7688092016-11-17 00:08:57 -0500332 if thestring.endswith(ending):
333 return thestring[:-len(ending)]
334 return thestring
335
336
Khen Nursimulubd24f552016-11-18 11:31:15 -0500337def traverse_desc(descriptor):
338 referenced_messages = []
339 name = rchop(descriptor.get('name', ''), '.proto')
Khen Nursimulud7688092016-11-17 00:08:57 -0500340 package = descriptor.get('package', '')
341 description = descriptor.get('_description', '')
Khen Nursimulubd24f552016-11-18 11:31:15 -0500342 messages = traverse_messages(descriptor.get('message_type', []),
343 package, referenced_messages)
344 enums = traverse_enums(descriptor.get('enum_type', []), package)
345 services = traverse_services(descriptor.get('service', []),
346 referenced_messages)
Khen Nursimulud7688092016-11-17 00:08:57 -0500347 # extensions = _traverse_extensions(descriptors)
348 # options = _traverse_options(descriptors)
Khen Nursimulubd24f552016-11-18 11:31:15 -0500349 set_messages_keys(messages)
350 unique_referred_messages_with_keys = []
351 for message_name in list(set(referenced_messages)):
352 unique_referred_messages_with_keys.append(
353 {
354 'name': message_name,
355 'key': get_message_key(message_name, messages)
356 }
357 )
Khen Nursimulud7688092016-11-17 00:08:57 -0500358
359 data = {
360 'name': name,
361 'package': package,
Khen Nursimulubd24f552016-11-18 11:31:15 -0500362 'description': description,
Khen Nursimulud7688092016-11-17 00:08:57 -0500363 'messages': messages,
364 'enums': enums,
365 'services': services,
Khen Nursimulubd24f552016-11-18 11:31:15 -0500366 'referenced_messages': list(set(referenced_messages)),
367 # TODO: simplify for easier jinja2 template use
368 'referred_messages_with_keys': unique_referred_messages_with_keys,
Khen Nursimulud7688092016-11-17 00:08:57 -0500369 # 'extensions': extensions,
370 # 'options': options
371 }
Khen Nursimulud7688092016-11-17 00:08:57 -0500372 return data
373
374
Khen Nursimulubd24f552016-11-18 11:31:15 -0500375def set_messages_keys(messages):
376 for message in messages:
377 message['key'] = _get_message_key(message)
378 if message['messages']:
379 set_messages_keys(message['messages'])
380
381
382def _get_message_key(message):
383 # assume key is first yang base type field
384 for field in message['fields']:
385 if not field['type_ref']:
386 return field['name']
387 # no key yet - search nested messaged
388 if message['messages']:
389 return get_message_key(message['messages'])
390 else:
391 return None
392
393
394def get_message_key(message_name, messages):
395 for message in messages:
396 if message_name == message['name']:
397 return message['key']
398 if message['messages']:
399 return get_message_key(message_name, message['messages'])
400 return None
401
402
Khen Nursimulud7688092016-11-17 00:08:57 -0500403def generate_code(request, response):
404 assert isinstance(request, plugin.CodeGeneratorRequest)
405
406 parser = DescriptorParser()
407
408 # idx = 1
409 for proto_file in request.proto_file:
410 native_data = parser.parse_file_descriptor(proto_file,
411 type_tag_name='_type',
412 fold_comments=True)
413
414 # print native_data
Khen Nursimulubd24f552016-11-18 11:31:15 -0500415 yang_data = traverse_desc(native_data)
Khen Nursimulud7688092016-11-17 00:08:57 -0500416
417 f = response.file.add()
Khen Nursimulubd24f552016-11-18 11:31:15 -0500418 # TODO: We should have a separate file for each output. There is an
Khen Nursimulud7688092016-11-17 00:08:57 -0500419 # issue reusing the same filename with an incremental suffix. Using
420 # a different file name works but not the actual proto file name
421 f.name = proto_file.name.replace('.proto', '.yang')
422 # f.name = '{}_{}{}'.format(_rchop(proto_file.name, '.proto'), idx,
423 # '.yang')
424 # idx += 1
425 f.content = template_yang.render(module=yang_data)
426
Khen Nursimulubd24f552016-11-18 11:31:15 -0500427
Khen Nursimulud7688092016-11-17 00:08:57 -0500428def get_yang_type(field):
429 type = field['type']
430 if type in YANG_TYPE_MAP.keys():
431 _type, _ = YANG_TYPE_MAP[type]
432 if _type in ['enumeration', 'message', 'group']:
433 return field['type_name'].split('.')[-1]
Khen Nursimulubd24f552016-11-18 11:31:15 -0500434 # return remove_first_character_if_match(field['type_name'],
435 # '.').replace('.', '-')
Khen Nursimulud7688092016-11-17 00:08:57 -0500436 else:
437 return _type
438 else:
439 return type
440
Khen Nursimulubd24f552016-11-18 11:31:15 -0500441
Khen Nursimulud7688092016-11-17 00:08:57 -0500442def is_base_type(type):
443 # check numeric value of the type first
444 if type in YANG_TYPE_MAP.keys():
445 _type, _ = YANG_TYPE_MAP[type]
446 return _type not in ['message', 'group']
447 else:
448 # proto name of the type
Khen Nursimulubd24f552016-11-18 11:31:15 -0500449 result = [_format for (_, _format) in YANG_TYPE_MAP.values() if
450 _format == type and _format not in ['message', 'group']]
Khen Nursimulud7688092016-11-17 00:08:57 -0500451 return len(result) > 0
452
Khen Nursimulubd24f552016-11-18 11:31:15 -0500453
Khen Nursimulud7688092016-11-17 00:08:57 -0500454def remove_unsupported_characters(text):
Khen Nursimulubd24f552016-11-18 11:31:15 -0500455 unsupported_characters = ["{", "}", "[", "]", "\"", "\\", "*", "/"]
456 return ''.join([i if i not in unsupported_characters else ' ' for i in
457 text])
458
459
460def remove_first_character_if_match(str, char):
461 if str.startswith(char):
462 return str[1:]
463 return str
464
Khen Nursimulud7688092016-11-17 00:08:57 -0500465
466YANG_TYPE_MAP = {
467 FieldDescriptor.TYPE_BOOL: ('boolean', 'boolean'),
468 FieldDescriptor.TYPE_BYTES: ('binary', 'byte'),
469 FieldDescriptor.TYPE_DOUBLE: ('decimal64', 'double'),
470 FieldDescriptor.TYPE_ENUM: ('enumeration', 'enum'),
471 FieldDescriptor.TYPE_FIXED32: ('int32', 'int64'),
472 FieldDescriptor.TYPE_FIXED64: ('int64', 'uint64'),
473 FieldDescriptor.TYPE_FLOAT: ('decimal64', 'float'),
474 FieldDescriptor.TYPE_INT32: ('int32', 'int32'),
475 FieldDescriptor.TYPE_INT64: ('int64', 'int64'),
476 FieldDescriptor.TYPE_SFIXED32: ('int32', 'int32'),
477 FieldDescriptor.TYPE_SFIXED64: ('int64', 'int64'),
478 FieldDescriptor.TYPE_STRING: ('string', 'string'),
479 FieldDescriptor.TYPE_SINT32: ('int32', 'int32'),
480 FieldDescriptor.TYPE_SINT64: ('int64', 'int64'),
481 FieldDescriptor.TYPE_UINT32: ('uint32', 'int64'),
482 FieldDescriptor.TYPE_UINT64: ('uint64', 'uint64'),
483 FieldDescriptor.TYPE_MESSAGE: ('message', 'message'),
484 FieldDescriptor.TYPE_GROUP: ('group', 'group')
485}
486
487if __name__ == '__main__':
488 # Read request message from stdin
489 data = sys.stdin.read()
490
491 # Parse request
492 request = plugin.CodeGeneratorRequest()
493 request.ParseFromString(data)
494
495 # Create response
496 response = plugin.CodeGeneratorResponse()
497
498 # Generate code
499 generate_code(request, response)
500
501 # Serialise response message
502 output = response.SerializeToString()
503
504 # Write to stdout
505 sys.stdout.write(output)
Khen Nursimulubd24f552016-11-18 11:31:15 -0500506 # print is_base_type(9)