tests: replace pycodestyle with black
[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         fields = []
162         for header in get_msg_header_defs(
163             struct_type_class, field_class, json_parser, logger
164         ):
165             logger.debug("Probing header `%s'" % header.name)
166             if header.is_part_of_def(m[1:]):
167                 self.header = header
168                 logger.debug("Found header `%s'" % header.name)
169                 fields.append(field_class(field_name="header", field_type=self.header))
170                 ignore = False
171                 break
172         if ignore and not self.is_event and not self.is_reply:
173             raise ParseError(
174                 "While parsing message `%s': could not find all "
175                 "common header fields" % name
176             )
177         for field in m[1:]:
178             if isinstance(field, dict) and "crc" in field:
179                 self.crc = field["crc"]
180                 logger.debug("Found CRC `%s'" % self.crc)
181                 continue
182             else:
183                 field_type = json_parser.lookup_type_like_id(field[0])
184                 logger.debug("Parsing message field `%s'" % field)
185                 l = len(field)
186                 if any(type(n) is dict for n in field):
187                     l -= 1
188                 if l == 2:
189                     if self.header is not None and self.header.has_field(field[1]):
190                         continue
191                     p = field_class(field_name=field[1], field_type=field_type)
192                 elif l == 3:
193                     if field[2] == 0 and field[0] != "string":
194                         raise ParseError(
195                             "While parsing message `%s': variable length "
196                             "array `%s' doesn't have reference to member "
197                             "containing the actual length" % (name, field[1])
198                         )
199                     if field[0] == "string" and field[2] > 0:
200                         field_type = json_parser.lookup_type_like_id("u8")
201
202                     p = field_class(
203                         field_name=field[1], field_type=field_type, array_len=field[2]
204                     )
205                 elif l == 4:
206                     nelem_field = None
207                     for f in fields:
208                         if f.name == field[3]:
209                             nelem_field = f
210                     if nelem_field is None:
211                         raise ParseError(
212                             "While parsing message `%s': couldn't find "
213                             "variable length array `%s' member containing "
214                             "the actual length `%s'" % (name, field[1], field[3])
215                         )
216                     p = field_class(
217                         field_name=field[1],
218                         field_type=field_type,
219                         array_len=field[2],
220                         nelem_field=nelem_field,
221                     )
222                 else:
223                     raise Exception(
224                         "Don't know how to parse message "
225                         "definition for message `%s': `%s'" % (m, m[1:])
226                     )
227                 logger.debug("Parsed field `%s'" % p)
228                 fields.append(p)
229         self.fields = fields
230         self.depends = [f.type for f in self.fields]
231         logger.debug("Parsed message: %s" % self)
232
233     def __str__(self):
234         return "Message(%s, [%s], {crc: %s}" % (
235             self.name,
236             "], [".join([str(f) for f in self.fields]),
237             self.crc,
238         )
239
240
241 class StructType(Type, Struct):
242     def __init__(self, definition, json_parser, field_class, logger):
243         t = definition
244         logger.debug("Parsing struct definition `%s'" % t)
245         name = t[0]
246         fields = []
247         for field in t[1:]:
248             if len(field) == 1 and "crc" in field:
249                 self.crc = field["crc"]
250                 continue
251             field_type = json_parser.lookup_type_like_id(field[0])
252             logger.debug("Parsing type field `%s'" % field)
253             if len(field) == 2:
254                 p = field_class(field_name=field[1], field_type=field_type)
255             elif len(field) == 3:
256                 if field[2] == 0:
257                     raise ParseError(
258                         "While parsing type `%s': array `%s' has "
259                         "variable length" % (name, field[1])
260                     )
261                 p = field_class(
262                     field_name=field[1], field_type=field_type, array_len=field[2]
263                 )
264             elif len(field) == 4:
265                 nelem_field = None
266                 for f in fields:
267                     if f.name == field[3]:
268                         nelem_field = f
269                 if nelem_field is None:
270                     raise ParseError(
271                         "While parsing message `%s': couldn't find "
272                         "variable length array `%s' member containing "
273                         "the actual length `%s'" % (name, field[1], field[3])
274                     )
275                 p = field_class(
276                     field_name=field[1],
277                     field_type=field_type,
278                     array_len=field[2],
279                     nelem_field=nelem_field,
280                 )
281             else:
282                 raise ParseError(
283                     "Don't know how to parse field `%s' of type definition "
284                     "for type `%s'" % (field, t)
285                 )
286             fields.append(p)
287         Type.__init__(self, name)
288         Struct.__init__(self, name, fields)
289
290     def __str__(self):
291         return "StructType(%s, %s)" % (Type.__str__(self), Struct.__str__(self))
292
293     def has_field(self, name):
294         return name in self.field_names
295
296     def is_part_of_def(self, definition):
297         for idx in range(len(self.fields)):
298             field = definition[idx]
299             p = self.fields[idx]
300             if field[1] != p.name:
301                 return False
302             if field[0] != p.type.name:
303                 raise ParseError(
304                     "Unexpected field type `%s' (should be `%s'), "
305                     "while parsing msg/def/field `%s/%s/%s'"
306                     % (field[0], p.type, p.name, definition, field)
307                 )
308         return True
309
310
311 class JsonParser(object):
312     def __init__(
313         self,
314         logger,
315         files,
316         simple_type_class=SimpleType,
317         enum_class=Enum,
318         union_class=Union,
319         struct_type_class=StructType,
320         field_class=Field,
321         message_class=Message,
322         alias_class=Alias,
323     ):
324         self.services = {}
325         self.messages = {}
326         self.enums = {}
327         self.unions = {}
328         self.aliases = {}
329         self.types = {
330             x: simple_type_class(x)
331             for x in [
332                 "i8",
333                 "i16",
334                 "i32",
335                 "i64",
336                 "u8",
337                 "u16",
338                 "u32",
339                 "u64",
340                 "f64",
341                 "bool",
342             ]
343         }
344
345         self.types["string"] = simple_type_class("vl_api_string_t")
346         self.replies = set()
347         self.events = set()
348         self.simple_type_class = simple_type_class
349         self.enum_class = enum_class
350         self.union_class = union_class
351         self.struct_type_class = struct_type_class
352         self.field_class = field_class
353         self.alias_class = alias_class
354         self.message_class = message_class
355
356         self.exceptions = []
357         self.json_files = []
358         self.types_by_json = {}
359         self.enums_by_json = {}
360         self.unions_by_json = {}
361         self.aliases_by_json = {}
362         self.messages_by_json = {}
363         self.logger = logger
364         for f in files:
365             self.parse_json_file(f)
366         self.finalize_parsing()
367
368     def parse_json_file(self, path):
369         self.logger.info("Parsing json api file: `%s'" % path)
370         self.json_files.append(path)
371         self.types_by_json[path] = []
372         self.enums_by_json[path] = []
373         self.unions_by_json[path] = []
374         self.aliases_by_json[path] = []
375         self.messages_by_json[path] = {}
376         with open(path) as f:
377             j = json.load(f)
378             for k in j["services"]:
379                 if k in self.services:
380                     raise ParseError("Duplicate service `%s'" % k)
381                 self.services[k] = j["services"][k]
382                 self.replies.add(self.services[k]["reply"])
383                 if "events" in self.services[k]:
384                     for x in self.services[k]["events"]:
385                         self.events.add(x)
386             for e in j["enums"]:
387                 name = e[0]
388                 value_pairs = e[1:-1]
389                 enumtype = self.types[e[-1]["enumtype"]]
390                 enum = self.enum_class(name, value_pairs, enumtype)
391                 self.enums[enum.name] = enum
392                 self.logger.debug("Parsed enum: %s" % enum)
393                 self.enums_by_json[path].append(enum)
394             exceptions = []
395             progress = 0
396             last_progress = 0
397             while True:
398                 for u in j["unions"]:
399                     name = u[0]
400                     if name in self.unions:
401                         progress = progress + 1
402                         continue
403                     try:
404                         type_pairs = [
405                             [self.lookup_type_like_id(t), n] for t, n in u[1:]
406                         ]
407                         union = self.union_class(name, type_pairs, 0)
408                         progress = progress + 1
409                     except ParseError as e:
410                         exceptions.append(e)
411                         continue
412                     self.unions[union.name] = union
413                     self.logger.debug("Parsed union: %s" % union)
414                     self.unions_by_json[path].append(union)
415                 for t in j["types"]:
416                     if t[0] in self.types:
417                         progress = progress + 1
418                         continue
419                     try:
420                         type_ = self.struct_type_class(
421                             t, self, self.field_class, self.logger
422                         )
423                         if type_.name in self.types:
424                             raise ParseError("Duplicate type `%s'" % type_.name)
425                         progress = progress + 1
426                     except ParseError as e:
427                         exceptions.append(e)
428                         continue
429                     self.types[type_.name] = type_
430                     self.types_by_json[path].append(type_)
431                     self.logger.debug("Parsed type: %s" % type_)
432                 for name, body in j["aliases"].items():
433                     if name in self.aliases:
434                         progress = progress + 1
435                         continue
436                     if "length" in body:
437                         array_len = body["length"]
438                     else:
439                         array_len = None
440                     try:
441                         t = self.lookup_type_like_id(body["type"])
442                     except ParseError as e:
443                         exceptions.append(e)
444                         continue
445                     alias = self.alias_class(name, t, array_len)
446                     self.aliases[name] = alias
447                     self.logger.debug("Parsed alias: %s" % alias)
448                     self.aliases_by_json[path].append(alias)
449                 if not exceptions:
450                     # finished parsing
451                     break
452                 if progress <= last_progress:
453                     # cannot make forward progress
454                     self.exceptions.extend(exceptions)
455                     break
456                 exceptions = []
457                 last_progress = progress
458                 progress = 0
459             prev_length = len(self.messages)
460             processed = []
461             while True:
462                 exceptions = []
463                 for m in j["messages"]:
464                     if m in processed:
465                         continue
466                     try:
467                         msg = self.message_class(self.logger, m, self)
468                         if msg.name in self.messages:
469                             raise ParseError("Duplicate message `%s'" % msg.name)
470                     except ParseError as e:
471                         exceptions.append(e)
472                         continue
473                     self.messages[msg.name] = msg
474                     self.messages_by_json[path][msg.name] = msg
475                     processed.append(m)
476                 if prev_length == len(self.messages):
477                     # cannot make forward progress ...
478                     self.exceptions.extend(exceptions)
479                     break
480                 prev_length = len(self.messages)
481
482     def lookup_type_like_id(self, name):
483         mundane_name = remove_magic(name)
484         if name in self.types:
485             return self.types[name]
486         elif name in self.enums:
487             return self.enums[name]
488         elif name in self.unions:
489             return self.unions[name]
490         elif name in self.aliases:
491             return self.aliases[name]
492         elif mundane_name in self.types:
493             return self.types[mundane_name]
494         elif mundane_name in self.enums:
495             return self.enums[mundane_name]
496         elif mundane_name in self.unions:
497             return self.unions[mundane_name]
498         elif mundane_name in self.aliases:
499             return self.aliases[mundane_name]
500         raise ParseError(
501             "Could not find type, enum or union by magic name `%s' nor by "
502             "mundane name `%s'" % (name, mundane_name)
503         )
504
505     def is_reply(self, message):
506         return message in self.replies
507
508     def is_event(self, message):
509         return message in self.events
510
511     def get_reply(self, message):
512         return self.messages[self.services[message]["reply"]]
513
514     def finalize_parsing(self):
515         if len(self.messages) == 0:
516             for e in self.exceptions:
517                 self.logger.warning(e)
518         for jn, j in self.messages_by_json.items():
519             remove = []
520             for n, m in j.items():
521                 try:
522                     if not m.is_reply and not m.is_event:
523                         try:
524                             m.reply = self.get_reply(n)
525                             if "stream" in self.services[m.name]:
526                                 m.reply_is_stream = self.services[m.name]["stream"]
527                             else:
528                                 m.reply_is_stream = False
529                             m.reply.request = m
530                         except:
531                             raise ParseError("Cannot find reply to message `%s'" % n)
532                 except ParseError as e:
533                     self.exceptions.append(e)
534                     remove.append(n)
535
536             self.messages_by_json[jn] = {k: v for k, v in j.items() if k not in remove}