00c234fc0148b816f4a7c5748d54befe376cbfa8
[vpp.git] / src / vpp-api / vapi / vapi_json_parser.py
1 #!/usr/bin/env python3
2
3 import json
4
5
6 class ParseError(Exception):
7     pass
8
9
10 magic_prefix = "vl_api_"
11 magic_suffix = "_t"
12
13
14 def remove_magic(what):
15     if what.startswith(magic_prefix) and what.endswith(magic_suffix):
16         return what[len(magic_prefix) : -len(magic_suffix)]
17     return what
18
19
20 class Field(object):
21     def __init__(self, field_name, field_type, array_len=None, nelem_field=None):
22         self.name = field_name
23         self.type = field_type
24         self.len = array_len
25         self.nelem_field = nelem_field
26
27     def __str__(self):
28         if self.len is None:
29             return "Field(name: %s, type: %s)" % (self.name, self.type)
30         elif type(self.len) == dict:
31             return "Field(name: %s, type: %s, length: %s)" % (
32                 self.name,
33                 self.type,
34                 self.len,
35             )
36         elif self.len > 0:
37             return "Field(name: %s, type: %s, length: %s)" % (
38                 self.name,
39                 self.type,
40                 self.len,
41             )
42         else:
43             return "Field(name: %s, type: %s, variable length stored in: %s)" % (
44                 self.name,
45                 self.type,
46                 self.nelem_field,
47             )
48
49     def is_vla(self):
50         return self.nelem_field is not None
51
52     def has_vla(self):
53         return self.is_vla() or self.type.has_vla()
54
55
56 class Alias(Field):
57     pass
58
59
60 class Type(object):
61     def __init__(self, name):
62         self.name = name
63
64     def __str__(self):
65         return self.name
66
67
68 class SimpleType(Type):
69     def has_vla(self):
70         return False
71
72
73 def get_msg_header_defs(struct_type_class, field_class, json_parser, logger):
74     return [
75         struct_type_class(
76             [
77                 "msg_header1_t",
78                 ["u16", "_vl_msg_id"],
79                 ["u32", "context"],
80             ],
81             json_parser,
82             field_class,
83             logger,
84         ),
85         struct_type_class(
86             [
87                 "msg_header2_t",
88                 ["u16", "_vl_msg_id"],
89                 ["u32", "client_index"],
90                 ["u32", "context"],
91             ],
92             json_parser,
93             field_class,
94             logger,
95         ),
96     ]
97
98
99 class Struct(object):
100     def __init__(self, name, fields):
101         self.name = name
102         self.fields = fields
103         self.field_names = [n.name for n in self.fields]
104         self.depends = [f.type for f in self.fields]
105
106     def __str__(self):
107         return "[%s]" % "], [".join([str(f) for f in self.fields])
108
109     def has_vla(self):
110         for f in self.fields:
111             if f.has_vla():
112                 return True
113         return False
114
115
116 class Enum(SimpleType):
117     def __init__(self, name, value_pairs, enumtype):
118         super(Enum, self).__init__(name)
119         self.type = enumtype
120         self.value_pairs = value_pairs
121
122     def __str__(self):
123         return "Enum(%s, [%s])" % (
124             self.name,
125             "], [".join(["%s => %s" % (i, j) for i, j in self.value_pairs]),
126         )
127
128
129 class Union(Type):
130     def __init__(self, name, type_pairs, crc):
131         Type.__init__(self, name)
132         self.crc = crc
133         self.type_pairs = type_pairs
134         self.depends = [t for t, _ in self.type_pairs]
135
136     def __str__(self):
137         return "Union(%s, [%s])" % (
138             self.name,
139             "], [".join(["%s %s" % (i, j) for i, j in self.type_pairs]),
140         )
141
142     def has_vla(self):
143         return False
144
145
146 class Message(object):
147     def __init__(self, logger, definition, json_parser):
148         struct_type_class = json_parser.struct_type_class
149         field_class = json_parser.field_class
150         self.request = None
151         self.logger = logger
152         m = definition
153         logger.debug("Parsing message definition `%s'" % m)
154         name = m[0]
155         self.name = name
156         logger.debug("Message name is `%s'" % name)
157         ignore = True
158         self.header = None
159         self.is_reply = json_parser.is_reply(self.name)
160         self.is_event = json_parser.is_event(self.name)
161         self.is_stream = json_parser.is_stream(self.name)
162         fields = []
163         for header in get_msg_header_defs(
164             struct_type_class, field_class, json_parser, logger
165         ):
166             logger.debug("Probing header `%s'" % header.name)
167             if header.is_part_of_def(m[1:]):
168                 self.header = header
169                 logger.debug("Found header `%s'" % header.name)
170                 fields.append(field_class(field_name="header", field_type=self.header))
171                 ignore = False
172                 break
173         if ignore and not self.is_event and not self.is_reply:
174             raise ParseError(
175                 "While parsing message `%s': could not find all "
176                 "common header fields" % name
177             )
178         for field in m[1:]:
179             if isinstance(field, dict) and "crc" in field:
180                 self.crc = field["crc"]
181                 logger.debug("Found CRC `%s'" % self.crc)
182                 continue
183             else:
184                 field_type = json_parser.lookup_type_like_id(field[0])
185                 logger.debug("Parsing message field `%s'" % field)
186                 l = len(field)
187                 if any(type(n) is dict for n in field):
188                     l -= 1
189                 if l == 2:
190                     if self.header is not None and self.header.has_field(field[1]):
191                         continue
192                     p = field_class(field_name=field[1], field_type=field_type)
193                 elif l == 3:
194                     if field[2] == 0 and field[0] != "string":
195                         raise ParseError(
196                             "While parsing message `%s': variable length "
197                             "array `%s' doesn't have reference to member "
198                             "containing the actual length" % (name, field[1])
199                         )
200                     if field[0] == "string" and field[2] > 0:
201                         field_type = json_parser.lookup_type_like_id("u8")
202
203                     p = field_class(
204                         field_name=field[1], field_type=field_type, array_len=field[2]
205                     )
206                 elif l == 4:
207                     nelem_field = None
208                     for f in fields:
209                         if f.name == field[3]:
210                             nelem_field = f
211                     if nelem_field is None:
212                         raise ParseError(
213                             "While parsing message `%s': couldn't find "
214                             "variable length array `%s' member containing "
215                             "the actual length `%s'" % (name, field[1], field[3])
216                         )
217                     p = field_class(
218                         field_name=field[1],
219                         field_type=field_type,
220                         array_len=field[2],
221                         nelem_field=nelem_field,
222                     )
223                 else:
224                     raise Exception(
225                         "Don't know how to parse message "
226                         "definition for message `%s': `%s'" % (m, m[1:])
227                     )
228                 logger.debug("Parsed field `%s'" % p)
229                 fields.append(p)
230         self.fields = fields
231         self.depends = [f.type for f in self.fields]
232         logger.debug("Parsed message: %s" % self)
233
234     def __str__(self):
235         return "Message(%s, [%s], {crc: %s}" % (
236             self.name,
237             "], [".join([str(f) for f in self.fields]),
238             self.crc,
239         )
240
241
242 class StructType(Type, Struct):
243     def __init__(self, definition, json_parser, field_class, logger):
244         t = definition
245         logger.debug("Parsing struct definition `%s'" % t)
246         name = t[0]
247         fields = []
248         for field in t[1:]:
249             if len(field) == 1 and "crc" in field:
250                 self.crc = field["crc"]
251                 continue
252             field_type = json_parser.lookup_type_like_id(field[0])
253             logger.debug("Parsing type field `%s'" % field)
254             if len(field) == 2:
255                 p = field_class(field_name=field[1], field_type=field_type)
256             elif len(field) == 3:
257                 if field[2] == 0:
258                     raise ParseError(
259                         "While parsing type `%s': array `%s' has "
260                         "variable length" % (name, field[1])
261                     )
262                 p = field_class(
263                     field_name=field[1], field_type=field_type, array_len=field[2]
264                 )
265             elif len(field) == 4:
266                 nelem_field = None
267                 for f in fields:
268                     if f.name == field[3]:
269                         nelem_field = f
270                 if nelem_field is None:
271                     raise ParseError(
272                         "While parsing message `%s': couldn't find "
273                         "variable length array `%s' member containing "
274                         "the actual length `%s'" % (name, field[1], field[3])
275                     )
276                 p = field_class(
277                     field_name=field[1],
278                     field_type=field_type,
279                     array_len=field[2],
280                     nelem_field=nelem_field,
281                 )
282             else:
283                 raise ParseError(
284                     "Don't know how to parse field `%s' of type definition "
285                     "for type `%s'" % (field, t)
286                 )
287             fields.append(p)
288         Type.__init__(self, name)
289         Struct.__init__(self, name, fields)
290
291     def __str__(self):
292         return "StructType(%s, %s)" % (Type.__str__(self), Struct.__str__(self))
293
294     def has_field(self, name):
295         return name in self.field_names
296
297     def is_part_of_def(self, definition):
298         for idx in range(len(self.fields)):
299             field = definition[idx]
300             p = self.fields[idx]
301             if field[1] != p.name:
302                 return False
303             if field[0] != p.type.name:
304                 raise ParseError(
305                     "Unexpected field type `%s' (should be `%s'), "
306                     "while parsing msg/def/field `%s/%s/%s'"
307                     % (field[0], p.type, p.name, definition, field)
308                 )
309         return True
310
311
312 class JsonParser(object):
313     def __init__(
314         self,
315         logger,
316         files,
317         simple_type_class=SimpleType,
318         enum_class=Enum,
319         union_class=Union,
320         struct_type_class=StructType,
321         field_class=Field,
322         message_class=Message,
323         alias_class=Alias,
324     ):
325         self.services = {}
326         self.messages = {}
327         self.enums = {}
328         self.enumflags = {}
329         self.unions = {}
330         self.aliases = {}
331         self.types = {
332             x: simple_type_class(x)
333             for x in [
334                 "i8",
335                 "i16",
336                 "i32",
337                 "i64",
338                 "u8",
339                 "u16",
340                 "u32",
341                 "u64",
342                 "f64",
343                 "bool",
344             ]
345         }
346
347         self.types["string"] = simple_type_class("vl_api_string_t")
348         self.replies = set()
349         self.events = set()
350         self.streams = set()
351         self.simple_type_class = simple_type_class
352         self.enum_class = enum_class
353         self.union_class = union_class
354         self.struct_type_class = struct_type_class
355         self.field_class = field_class
356         self.alias_class = alias_class
357         self.message_class = message_class
358
359         self.exceptions = []
360         self.json_files = []
361         self.types_by_json = {}
362         self.enums_by_json = {}
363         self.unions_by_json = {}
364         self.aliases_by_json = {}
365         self.messages_by_json = {}
366         self.logger = logger
367         for f in files:
368             self.parse_json_file(f)
369         self.finalize_parsing()
370
371     def parse_json_file(self, path):
372         self.logger.info("Parsing json api file: `%s'" % path)
373         self.json_files.append(path)
374         self.types_by_json[path] = []
375         self.enums_by_json[path] = []
376         self.unions_by_json[path] = []
377         self.aliases_by_json[path] = []
378         self.messages_by_json[path] = {}
379         with open(path) as f:
380             j = json.load(f)
381             for k in j["services"]:
382                 if k in self.services:
383                     raise ParseError("Duplicate service `%s'" % k)
384                 self.services[k] = j["services"][k]
385                 self.replies.add(self.services[k]["reply"])
386                 if "events" in self.services[k]:
387                     for x in self.services[k]["events"]:
388                         self.events.add(x)
389                 if "stream_msg" in self.services[k]:
390                     self.streams.add(self.services[k]["stream_msg"])
391             for e in j["enums"]:
392                 name = e[0]
393                 value_pairs = e[1:-1]
394                 enumtype = self.types[e[-1]["enumtype"]]
395                 enum = self.enum_class(name, value_pairs, enumtype)
396                 self.enums[enum.name] = enum
397                 self.logger.debug("Parsed enum: %s" % enum)
398                 self.enums_by_json[path].append(enum)
399             for e in j["enumflags"]:
400                 name = e[0]
401                 value_pairs = e[1:-1]
402                 enumtype = self.types[e[-1]["enumtype"]]
403                 enum = self.enum_class(name, value_pairs, enumtype)
404                 self.enums[enum.name] = enum
405                 self.logger.debug("Parsed enumflag: %s" % enum)
406                 self.enums_by_json[path].append(enum)
407             exceptions = []
408             progress = 0
409             last_progress = 0
410             while True:
411                 for u in j["unions"]:
412                     name = u[0]
413                     if name in self.unions:
414                         progress = progress + 1
415                         continue
416                     try:
417                         type_pairs = [
418                             [self.lookup_type_like_id(t), n] for t, n in u[1:]
419                         ]
420                         union = self.union_class(name, type_pairs, 0)
421                         progress = progress + 1
422                     except ParseError as e:
423                         exceptions.append(e)
424                         continue
425                     self.unions[union.name] = union
426                     self.logger.debug("Parsed union: %s" % union)
427                     self.unions_by_json[path].append(union)
428                 for t in j["types"]:
429                     if t[0] in self.types:
430                         progress = progress + 1
431                         continue
432                     try:
433                         type_ = self.struct_type_class(
434                             t, self, self.field_class, self.logger
435                         )
436                         if type_.name in self.types:
437                             raise ParseError("Duplicate type `%s'" % type_.name)
438                         progress = progress + 1
439                     except ParseError as e:
440                         exceptions.append(e)
441                         continue
442                     self.types[type_.name] = type_
443                     self.types_by_json[path].append(type_)
444                     self.logger.debug("Parsed type: %s" % type_)
445                 for name, body in j["aliases"].items():
446                     if name in self.aliases:
447                         progress = progress + 1
448                         continue
449                     if "length" in body:
450                         array_len = body["length"]
451                     else:
452                         array_len = None
453                     try:
454                         t = self.lookup_type_like_id(body["type"])
455                     except ParseError as e:
456                         exceptions.append(e)
457                         continue
458                     alias = self.alias_class(name, t, array_len)
459                     self.aliases[name] = alias
460                     self.logger.debug("Parsed alias: %s" % alias)
461                     self.aliases_by_json[path].append(alias)
462                 if not exceptions:
463                     # finished parsing
464                     break
465                 if progress <= last_progress:
466                     # cannot make forward progress
467                     self.exceptions.extend(exceptions)
468                     break
469                 exceptions = []
470                 last_progress = progress
471                 progress = 0
472             prev_length = len(self.messages)
473             processed = []
474             while True:
475                 exceptions = []
476                 for m in j["messages"]:
477                     if m in processed:
478                         continue
479                     try:
480                         msg = self.message_class(self.logger, m, self)
481                         if msg.name in self.messages:
482                             raise ParseError("Duplicate message `%s'" % msg.name)
483                     except ParseError as e:
484                         exceptions.append(e)
485                         continue
486                     self.messages[msg.name] = msg
487                     self.messages_by_json[path][msg.name] = msg
488                     processed.append(m)
489                 if prev_length == len(self.messages):
490                     # cannot make forward progress ...
491                     self.exceptions.extend(exceptions)
492                     break
493                 prev_length = len(self.messages)
494
495     def lookup_type_like_id(self, name):
496         mundane_name = remove_magic(name)
497         if name in self.types:
498             return self.types[name]
499         elif name in self.enums:
500             return self.enums[name]
501         elif name in self.enumflags:
502             return self.enumflags[name]
503         elif name in self.unions:
504             return self.unions[name]
505         elif name in self.aliases:
506             return self.aliases[name]
507         elif mundane_name in self.types:
508             return self.types[mundane_name]
509         elif mundane_name in self.enums:
510             return self.enums[mundane_name]
511         elif mundane_name in self.enumflags:
512             return self.enumflags[mundane_name]
513         elif mundane_name in self.unions:
514             return self.unions[mundane_name]
515         elif mundane_name in self.aliases:
516             return self.aliases[mundane_name]
517         raise ParseError(
518             "Could not find type, enum or union by magic name `%s' nor by "
519             "mundane name `%s'" % (name, mundane_name)
520         )
521
522     def is_reply(self, message):
523         return message in self.replies
524
525     def is_event(self, message):
526         return message in self.events
527
528     def is_stream(self, message):
529         return message in self.streams
530
531     def has_stream_msg(self, message):
532         return (
533             message.name in self.services
534             and "stream_msg" in self.services[message.name]
535         )
536
537     def get_stream_msg(self, message):
538         if not self.has_stream_msg(message):
539             return None
540         return self.messages[self.services[message.name]["stream_msg"]]
541
542     def get_reply(self, message):
543         return self.messages[self.services[message]["reply"]]
544
545     def finalize_parsing(self):
546         if len(self.messages) == 0:
547             for e in self.exceptions:
548                 self.logger.warning(e)
549         for jn, j in self.messages_by_json.items():
550             remove = []
551             for n, m in j.items():
552                 try:
553                     if not m.is_reply and not m.is_event and not m.is_stream:
554                         try:
555                             m.reply = self.get_reply(n)
556                             m.reply_is_stream = False
557                             m.has_stream_msg = self.has_stream_msg(m)
558                             if "stream" in self.services[m.name]:
559                                 m.reply_is_stream = self.services[m.name]["stream"]
560                             if m.has_stream_msg:
561                                 m.stream_msg = self.get_stream_msg(m)
562                             m.reply.request = m
563                         except:
564                             raise ParseError("Cannot find reply to message `%s'" % n)
565                 except ParseError as e:
566                     self.exceptions.append(e)
567                     remove.append(n)
568
569             self.messages_by_json[jn] = {k: v for k, v in j.items() if k not in remove}