2
0
Эх сурвалжийг харах

qapi: Add a primitive to include other files from a QAPI schema file

The primitive uses JSON syntax, and include paths are relative to the file using the directive:

  { 'include': 'path/to/file.json' }

Signed-off-by: Lluís Vilanova <vilanova@ac.upc.edu>
Reviewed-by: Eric Blake <eblake@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Luiz Capitulino <lcapitulino@redhat.com>
Lluís Vilanova 11 жил өмнө
parent
commit
a719a27c82
44 өөрчлөгдсөн 110 нэмэгдсэн , 13 устгасан
  1. 11 0
      docs/qapi-code-gen.txt
  2. 52 12
      scripts/qapi.py
  3. 4 1
      tests/Makefile
  4. 1 0
      tests/qapi-schema/include-before-err.err
  5. 1 0
      tests/qapi-schema/include-before-err.exit
  6. 2 0
      tests/qapi-schema/include-before-err.json
  7. 0 0
      tests/qapi-schema/include-before-err.out
  8. 1 0
      tests/qapi-schema/include-cycle-b.json
  9. 1 0
      tests/qapi-schema/include-cycle-c.json
  10. 3 0
      tests/qapi-schema/include-cycle.err
  11. 1 0
      tests/qapi-schema/include-cycle.exit
  12. 1 0
      tests/qapi-schema/include-cycle.json
  13. 0 0
      tests/qapi-schema/include-cycle.out
  14. 1 0
      tests/qapi-schema/include-format-err.err
  15. 1 0
      tests/qapi-schema/include-format-err.exit
  16. 2 0
      tests/qapi-schema/include-format-err.json
  17. 0 0
      tests/qapi-schema/include-format-err.out
  18. 2 0
      tests/qapi-schema/include-nested-err.err
  19. 1 0
      tests/qapi-schema/include-nested-err.exit
  20. 1 0
      tests/qapi-schema/include-nested-err.json
  21. 0 0
      tests/qapi-schema/include-nested-err.out
  22. 1 0
      tests/qapi-schema/include-no-file.err
  23. 1 0
      tests/qapi-schema/include-no-file.exit
  24. 1 0
      tests/qapi-schema/include-no-file.json
  25. 0 0
      tests/qapi-schema/include-no-file.out
  26. 1 0
      tests/qapi-schema/include-non-file.err
  27. 1 0
      tests/qapi-schema/include-non-file.exit
  28. 1 0
      tests/qapi-schema/include-non-file.json
  29. 0 0
      tests/qapi-schema/include-non-file.out
  30. 2 0
      tests/qapi-schema/include-relpath-sub.json
  31. 0 0
      tests/qapi-schema/include-relpath.err
  32. 1 0
      tests/qapi-schema/include-relpath.exit
  33. 1 0
      tests/qapi-schema/include-relpath.json
  34. 3 0
      tests/qapi-schema/include-relpath.out
  35. 1 0
      tests/qapi-schema/include-self-cycle.err
  36. 1 0
      tests/qapi-schema/include-self-cycle.exit
  37. 1 0
      tests/qapi-schema/include-self-cycle.json
  38. 0 0
      tests/qapi-schema/include-self-cycle.out
  39. 2 0
      tests/qapi-schema/include-simple-sub.json
  40. 0 0
      tests/qapi-schema/include-simple.err
  41. 1 0
      tests/qapi-schema/include-simple.exit
  42. 1 0
      tests/qapi-schema/include-simple.json
  43. 3 0
      tests/qapi-schema/include-simple.out
  44. 1 0
      tests/qapi-schema/include/relpath.json

+ 11 - 0
docs/qapi-code-gen.txt

@@ -40,6 +40,17 @@ enumeration types and union types.
 Generally speaking, types definitions should always use CamelCase for the type
 Generally speaking, types definitions should always use CamelCase for the type
 names. Command names should be all lower case with words separated by a hyphen.
 names. Command names should be all lower case with words separated by a hyphen.
 
 
+
+=== Includes ===
+
+The QAPI schema definitions can be modularized using the 'include' directive:
+
+ { 'include': 'path/to/file.json'}
+
+The directive is evaluated recursively, and include paths are relative to the
+file using the directive.
+
+
 === Complex types ===
 === Complex types ===
 
 
 A complex type is a dictionary containing a single key whose value is a
 A complex type is a dictionary containing a single key whose value is a

+ 52 - 12
scripts/qapi.py

@@ -11,6 +11,7 @@
 # This work is licensed under the terms of the GNU GPL, version 2.
 # This work is licensed under the terms of the GNU GPL, version 2.
 # See the COPYING file in the top-level directory.
 # See the COPYING file in the top-level directory.
 
 
+import re
 from ordereddict import OrderedDict
 from ordereddict import OrderedDict
 import os
 import os
 import sys
 import sys
@@ -36,9 +37,17 @@
     'uint64':   'QTYPE_QINT',
     'uint64':   'QTYPE_QINT',
 }
 }
 
 
+def error_path(parent):
+    res = ""
+    while parent:
+        res = ("In file included from %s:%d:\n" % (parent['file'],
+                                                   parent['line'])) + res
+        parent = parent['parent']
+    return res
+
 class QAPISchemaError(Exception):
 class QAPISchemaError(Exception):
     def __init__(self, schema, msg):
     def __init__(self, schema, msg):
-        self.fp = schema.fp
+        self.input_file = schema.input_file
         self.msg = msg
         self.msg = msg
         self.col = 1
         self.col = 1
         self.line = schema.line
         self.line = schema.line
@@ -47,23 +56,31 @@ def __init__(self, schema, msg):
                 self.col = (self.col + 7) % 8 + 1
                 self.col = (self.col + 7) % 8 + 1
             else:
             else:
                 self.col += 1
                 self.col += 1
+        self.info = schema.parent_info
 
 
     def __str__(self):
     def __str__(self):
-        return "%s:%s:%s: %s" % (self.fp.name, self.line, self.col, self.msg)
+        return error_path(self.info) + \
+            "%s:%d:%d: %s" % (self.input_file, self.line, self.col, self.msg)
 
 
 class QAPIExprError(Exception):
 class QAPIExprError(Exception):
     def __init__(self, expr_info, msg):
     def __init__(self, expr_info, msg):
-        self.fp = expr_info['fp']
-        self.line = expr_info['line']
+        self.info = expr_info
         self.msg = msg
         self.msg = msg
 
 
     def __str__(self):
     def __str__(self):
-        return "%s:%s: %s" % (self.fp.name, self.line, self.msg)
+        return error_path(self.info['parent']) + \
+            "%s:%d: %s" % (self.info['file'], self.info['line'], self.msg)
 
 
 class QAPISchema:
 class QAPISchema:
 
 
-    def __init__(self, fp):
-        self.fp = fp
+    def __init__(self, fp, input_relname=None, include_hist=[], parent_info=None):
+        input_fname = os.path.abspath(fp.name)
+        if input_relname is None:
+            input_relname = fp.name
+        self.input_dir = os.path.dirname(input_fname)
+        self.input_file = input_relname
+        self.include_hist = include_hist + [(input_relname, input_fname)]
+        self.parent_info = parent_info
         self.src = fp.read()
         self.src = fp.read()
         if self.src == '' or self.src[-1] != '\n':
         if self.src == '' or self.src[-1] != '\n':
             self.src += '\n'
             self.src += '\n'
@@ -74,10 +91,33 @@ def __init__(self, fp):
         self.accept()
         self.accept()
 
 
         while self.tok != None:
         while self.tok != None:
-            expr_info = {'fp': fp, 'line': self.line}
-            expr_elem = {'expr': self.get_expr(False),
-                         'info': expr_info}
-            self.exprs.append(expr_elem)
+            expr_info = {'file': input_relname, 'line': self.line, 'parent': self.parent_info}
+            expr = self.get_expr(False)
+            if isinstance(expr, dict) and "include" in expr:
+                if len(expr) != 1:
+                    raise QAPIExprError(expr_info, "Invalid 'include' directive")
+                include = expr["include"]
+                if not isinstance(include, str):
+                    raise QAPIExprError(expr_info,
+                                        'Expected a file name (string), got: %s'
+                                        % include)
+                include_path = os.path.join(self.input_dir, include)
+                if any(include_path == elem[1]
+                       for elem in self.include_hist):
+                    raise QAPIExprError(expr_info, "Inclusion loop for %s"
+                                        % include)
+                try:
+                    fobj = open(include_path, 'r')
+                except IOError as e:
+                    raise QAPIExprError(expr_info,
+                                        '%s: %s' % (e.strerror, include))
+                exprs_include = QAPISchema(fobj, include,
+                                           self.include_hist, expr_info)
+                self.exprs.extend(exprs_include.exprs)
+            else:
+                expr_elem = {'expr': expr,
+                             'info': expr_info}
+                self.exprs.append(expr_elem)
 
 
     def accept(self):
     def accept(self):
         while True:
         while True:
@@ -267,7 +307,7 @@ def check_exprs(schema):
 def parse_schema(input_file):
 def parse_schema(input_file):
     try:
     try:
         schema = QAPISchema(open(input_file, "r"))
         schema = QAPISchema(open(input_file, "r"))
-    except QAPISchemaError, e:
+    except (QAPISchemaError, QAPIExprError), e:
         print >>sys.stderr, e
         print >>sys.stderr, e
         exit(1)
         exit(1)
 
 

+ 4 - 1
tests/Makefile

@@ -190,7 +190,10 @@ check-qapi-schema-y := $(addprefix tests/qapi-schema/, \
         duplicate-key.json union-invalid-base.json flat-union-no-base.json \
         duplicate-key.json union-invalid-base.json flat-union-no-base.json \
         flat-union-invalid-discriminator.json \
         flat-union-invalid-discriminator.json \
         flat-union-invalid-branch-key.json flat-union-reverse-define.json \
         flat-union-invalid-branch-key.json flat-union-reverse-define.json \
-        flat-union-string-discriminator.json)
+        flat-union-string-discriminator.json \
+        include-simple.json include-relpath.json include-format-err.json \
+        include-non-file.json include-no-file.json include-before-err.json \
+        include-nested-err.json include-self-cycle.json include-cycle.json)
 
 
 GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h tests/test-qmp-commands.h
 GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h tests/test-qmp-commands.h
 
 

+ 1 - 0
tests/qapi-schema/include-before-err.err

@@ -0,0 +1 @@
+tests/qapi-schema/include-before-err.json:2:13: Expected ":"

+ 1 - 0
tests/qapi-schema/include-before-err.exit

@@ -0,0 +1 @@
+1

+ 2 - 0
tests/qapi-schema/include-before-err.json

@@ -0,0 +1,2 @@
+{ 'include': 'include-simple-sub.json' }
+{ 'command' 'missing-colon' }

+ 0 - 0
tests/qapi-schema/include-before-err.out


+ 1 - 0
tests/qapi-schema/include-cycle-b.json

@@ -0,0 +1 @@
+{ 'include': 'include-cycle-c.json' }

+ 1 - 0
tests/qapi-schema/include-cycle-c.json

@@ -0,0 +1 @@
+{ 'include': 'include-cycle.json' }

+ 3 - 0
tests/qapi-schema/include-cycle.err

@@ -0,0 +1,3 @@
+In file included from tests/qapi-schema/include-cycle.json:1:
+In file included from include-cycle-b.json:1:
+include-cycle-c.json:1: Inclusion loop for include-cycle.json

+ 1 - 0
tests/qapi-schema/include-cycle.exit

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/qapi-schema/include-cycle.json

@@ -0,0 +1 @@
+{ 'include': 'include-cycle-b.json' }

+ 0 - 0
tests/qapi-schema/include-cycle.out


+ 1 - 0
tests/qapi-schema/include-format-err.err

@@ -0,0 +1 @@
+tests/qapi-schema/include-format-err.json:1: Invalid 'include' directive

+ 1 - 0
tests/qapi-schema/include-format-err.exit

@@ -0,0 +1 @@
+1

+ 2 - 0
tests/qapi-schema/include-format-err.json

@@ -0,0 +1,2 @@
+{ 'include': 'include-simple-sub.json',
+  'foo': 'bar' }

+ 0 - 0
tests/qapi-schema/include-format-err.out


+ 2 - 0
tests/qapi-schema/include-nested-err.err

@@ -0,0 +1,2 @@
+In file included from tests/qapi-schema/include-nested-err.json:1:
+missing-colon.json:1:10: Expected ":"

+ 1 - 0
tests/qapi-schema/include-nested-err.exit

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/qapi-schema/include-nested-err.json

@@ -0,0 +1 @@
+{ 'include': 'missing-colon.json' }

+ 0 - 0
tests/qapi-schema/include-nested-err.out


+ 1 - 0
tests/qapi-schema/include-no-file.err

@@ -0,0 +1 @@
+tests/qapi-schema/include-no-file.json:1: No such file or directory: include-no-file-sub.json

+ 1 - 0
tests/qapi-schema/include-no-file.exit

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/qapi-schema/include-no-file.json

@@ -0,0 +1 @@
+{ 'include': 'include-no-file-sub.json' }

+ 0 - 0
tests/qapi-schema/include-no-file.out


+ 1 - 0
tests/qapi-schema/include-non-file.err

@@ -0,0 +1 @@
+tests/qapi-schema/include-non-file.json:1: Expected a file name (string), got: ['foo', 'bar']

+ 1 - 0
tests/qapi-schema/include-non-file.exit

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/qapi-schema/include-non-file.json

@@ -0,0 +1 @@
+{ 'include': [ 'foo', 'bar' ] }

+ 0 - 0
tests/qapi-schema/include-non-file.out


+ 2 - 0
tests/qapi-schema/include-relpath-sub.json

@@ -0,0 +1,2 @@
+{ 'enum': 'Status',
+  'data': [ 'good', 'bad', 'ugly' ] }

+ 0 - 0
tests/qapi-schema/include-relpath.err


+ 1 - 0
tests/qapi-schema/include-relpath.exit

@@ -0,0 +1 @@
+0

+ 1 - 0
tests/qapi-schema/include-relpath.json

@@ -0,0 +1 @@
+{ 'include': 'include/relpath.json' }

+ 3 - 0
tests/qapi-schema/include-relpath.out

@@ -0,0 +1,3 @@
+[OrderedDict([('enum', 'Status'), ('data', ['good', 'bad', 'ugly'])])]
+[{'enum_name': 'Status', 'enum_values': ['good', 'bad', 'ugly']}]
+[]

+ 1 - 0
tests/qapi-schema/include-self-cycle.err

@@ -0,0 +1 @@
+tests/qapi-schema/include-self-cycle.json:1: Inclusion loop for include-self-cycle.json

+ 1 - 0
tests/qapi-schema/include-self-cycle.exit

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/qapi-schema/include-self-cycle.json

@@ -0,0 +1 @@
+{ 'include': 'include-self-cycle.json' }

+ 0 - 0
tests/qapi-schema/include-self-cycle.out


+ 2 - 0
tests/qapi-schema/include-simple-sub.json

@@ -0,0 +1,2 @@
+{ 'enum': 'Status',
+  'data': [ 'good', 'bad', 'ugly' ] }

+ 0 - 0
tests/qapi-schema/include-simple.err


+ 1 - 0
tests/qapi-schema/include-simple.exit

@@ -0,0 +1 @@
+0

+ 1 - 0
tests/qapi-schema/include-simple.json

@@ -0,0 +1 @@
+{ 'include': 'include-simple-sub.json' }

+ 3 - 0
tests/qapi-schema/include-simple.out

@@ -0,0 +1,3 @@
+[OrderedDict([('enum', 'Status'), ('data', ['good', 'bad', 'ugly'])])]
+[{'enum_name': 'Status', 'enum_values': ['good', 'bad', 'ugly']}]
+[]

+ 1 - 0
tests/qapi-schema/include/relpath.json

@@ -0,0 +1 @@
+{ 'include': '../include-relpath-sub.json' }